<?php
namespace CorpusParole\Libraries\Mergers;


use EasyRdf\RdfNamespace;
use EasyRdf\Graph;

RdfNamespace::set('edm', 'http://www.europeana.eu/schemas/edm/');
RdfNamespace::set('ore', 'http://www.openarchives.org/ore/terms/');
RdfNamespace::set('crdo', 'http://crdo.risc.cnrs.fr/schemas/');
RdfNamespace::set('olac', 'http://www.language-archives.org/OLAC/1.1/');
RdfNamespace::set('skos', 'http://www.w3.org/2004/02/skos/core#');

abstract class CocoonAbstractRdfMerger implements RdfMerger {

    const ORIGIN_BASE = 0;
    const ORIGIN_SRC = 1;

    abstract protected function getTypeMergeMethodMap();

    /**
     * Merge an Rdf Graph from one model to another
     *
     * @param EasyRdf\Graph $baseGraph The graph used as base for the merge
     * @param EasyRdf\Graph $srcGraph The source graph with the new triples to add
     *
     * @return EasyRdf\Graph The new merged graph
     */
    function mergeGraph($baseGraph, $srcGraph, $uri = null) {

        $uri = is_null($uri)?$baseGraph->getUri():$uri;
        $this->mergedArray = [];
        $this->resGraph = new Graph($uri);
        $this->bnodeMerge = [];
        $this->baseGraph = $baseGraph;
        $this->srcGraph = $srcGraph;


        $typeMergeMethodMap = $this->getTypeMergeMethodMap();

        foreach ( $typeMergeMethodMap as $nodeType => $mergeMethod) {

            foreach($baseGraph->allOfType($nodeType) as $baseResource) {
                if($baseResource->isBNode()) {
                    continue;
                }
                $this->mergedArray[$baseResource->getUri()] = $baseGraph->toRdfPhp()[$baseResource->getUri()];
            }
            foreach($srcGraph->allOfType($nodeType) as $srcResource) {
                if($srcResource->isBNode()) {
                    continue;
                }
                if(empty($baseGraph->propertyUris($srcResource->getUri()))) {
                    $this->mergedArray[$srcResource->getUri()] = $srcGraph->toRdfPhp()[$srcResource->getUri()];
                }
                else {
                    $baseResource = $baseGraph->resource($srcResource->getUri());
                    $this->mergedArray[$srcResource->getUri()] = $baseGraph->toRdfPhp()[$baseResource->getUri()];
                    call_user_func(array($this, $mergeMethod), $baseResource, $srcResource);
                }
            }
        }

        // merge blank node
        reset($this->bnodeMerge);
        while(list($bnodeId, $bnodeDef) = each($this->bnodeMerge)) {

            $srcUrl = isset($bnodeDef['src_url'])?$bnodeDef['src_url']:null;
            $baseUrl = isset($bnodeDef['base_url'])?$bnodeDef['base_url']:null;

            if(is_null($srcUrl) && !is_null($baseUrl)) {
                $copyResource = $this->copyResource($baseGraph->toRdfPhp()[$baseUrl],CocoonAbstractRdfMerger::ORIGIN_BASE);
                $this->mergedArray[$bnodeId] = $copyResource;
            }
            elseif (is_null($baseUrl) && !is_null($srcUrl)) {
                $copyResource = $this->copyResource($srcGraph->toRdfPhp()[$srcUrl],CocoonAbstractRdfMerger::ORIGIN_SRC);
                $this->mergedArray[$bnodeId] = $copyResource;
            }
            elseif (!is_null($baseUrl) && !is_null($srcUrl)) {

                $baseResource = $baseGraph->resource($baseUrl);
                $srcResource = $srcGraph->resource($srcUrl);

                if(!is_null($baseResource->typeAsResource()) && array_key_exists($baseResource->typeAsResource()->getUri(), $typeMergeMethodMap)) {

                    $mergeMethod = $typeMergeMethodMap[$baseResource->typeAsResource()->getUri()];
                    $this->mergedArray[$bnodeId] = [];

                    call_user_func(array($this, $mergeMethod), $baseResource, $srcResource, $bnodeId);
                }
            }

        }

        //echo "MERGED ARRAY:\n";
        $this->resGraph->parse($this->mergedArray);
        return $this->resGraph;
    }

    /**
     * Copy a full resource node
     *
     */
    protected function copyResource($origArray, $origin) {
        $resArray = [];
        foreach($origArray as $prop => $propValues) {
            $resArray[$prop] = $this->buildValueList($propValues, $origin);
        }
        return $resArray;
    }

    /**
     * Build a value list. replace the blank node reference.
     * @param $srcValues array The original values
     * @param $origin int values are 'ORIGIN_BASE' if bnode from base graph or 'ORIGIN_SRC' from source graph
     */
    protected function buildValueList($srcValues, $origin) {
        $srcArrayProps = [];
        foreach ($srcValues as $propValue) {
            if(is_array($propValue) && array_key_exists('type', $propValue) && $propValue['type'] == 'bnode') {
                $newBNodeId = $this->resGraph->newBNodeId();
                if($origin == CocoonAbstractRdfMerger::ORIGIN_SRC) {
                    $this->bnodeMerge[$newBNodeId] = ['src_url' => $propValue['value'], 'base_url' => null];
                }
                else {
                    $this->bnodeMerge[$newBNodeId] = ['base_url' => $propValue['value'], 'src_url' => null];
                }

                $propValue['value'] = $newBNodeId;
            }
            $srcArrayProps[] = $propValue;
        }
        return $srcArrayProps;
    }

    protected function mergePropertySingleValue($prop, &$targetArray, $baseArray, $srcArray) {

        if(isset($baseArray[$prop])) {
            $targetArray[$prop] = $this->buildValueList($baseArray[$prop], CocoonAbstractRdfMerger::ORIGIN_BASE);
        }
        elseif(isset($srcArray[$prop])) {
            $targetArray[$prop] = $this->buildValueList($srcArray[$prop], CocoonAbstractRdfMerger::ORIGIN_SRC);
        }
    }

    protected function mergePropertyMultiplevalue($prop, &$targetArray, $baseArray, $srcArray) {
        $propArray = $this->buildValueList(isset($baseArray[$prop])?$baseArray[$prop]:[], CocoonAbstractRdfMerger::ORIGIN_BASE);
        if(isset($srcArray[$prop])) {
            $mergedArray = array_merge($propArray, $this->buildValueList($srcArray[$prop], CocoonAbstractRdfMerger::ORIGIN_BASE));
            //yes, this is real. Array_unique does not work on multidimentional arrays. Most work-around suggest the use of serialize to compare sub arrays...
            $propArray = array_values(array_intersect_key($mergedArray, array_unique(array_map('md5',array_map('serialize', $mergedArray)))));
        }

        if(!empty($propArray)) {
            $targetArray[$prop] = $propArray;
        }
    }

    protected function mergePropertySingleBNode($prop, &$targetArray, $baseArray, $srcArray) {

        $newBNodeId = $this->resGraph->newBNodeId();

        $srcUrl = null;
        $baseUrl = null;

        // in src but not in base
        if(array_key_exists($prop, $baseArray) &&
            !empty($baseArray[$prop]) &&
            array_key_exists('type',$baseArray[$prop][0]) &&
            array_key_exists('value',$baseArray[$prop][0]) &&
            $baseArray[$prop][0]['type'] === 'bnode') {
            $baseUrl = $baseArray[$prop][0]['value'];
        }

        if(array_key_exists($prop, $srcArray) &&
            !empty($srcArray[$prop]) &&
            array_key_exists('type',$srcArray[$prop][0]) &&
            array_key_exists('value',$srcArray[$prop][0]) &&
            $srcArray[$prop][0]['type'] === 'bnode') {
            $srcUrl = $srcArray[$prop][0]['value'];
        }

        $this->bnodeMerge[$newBNodeId] = ['src_url' => $srcUrl, 'base_url' => $baseUrl];

        $targetArray[$prop] = [ [ 'type' => 'bnode', 'value' => $newBNodeId], ];

    }


    protected function mergeProperties($singleBNodeProperties, $singleProperties, &$targetArray, $baseRes, $srcRes) {
        $srcRdfPhp = $this->srcGraph->toRdfPhp();
        $srcArray = array_key_exists($srcRes->getUri(), $srcRdfPhp)?$srcRdfPhp[$srcRes->getUri()]:[];
        $baseRdfPhp = $this->baseGraph->toRdfPhp();
        $baseArray = array_key_exists($baseRes->getUri(), $baseRdfPhp)?$baseRdfPhp[$baseRes->getUri()]:[];
        foreach(array_unique(array_merge($srcRes->propertyUris(), $baseRes->propertyUris())) as $prop) {
            if(in_array($prop, $singleBNodeProperties)) {
                $this->mergePropertySingleBNode($prop, $targetArray, $baseArray, $srcArray);
            }
            elseif(in_array($prop, $singleProperties)) {
                $this->mergePropertySingleValue($prop, $targetArray, $baseArray, $srcArray);
            }
            else {
                $this->mergePropertyMultiplevalue($prop, $targetArray, $baseArray, $srcArray);
            }
        }
    }
}
