/*
 *
 *  * Copyright 2010-2014 Orient Technologies LTD (info(at)orientechnologies.com)
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  *      http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *
 */

package com.orientechnologies.orient.etl.transformer;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.storage.ORecordDuplicatedException;
import com.orientechnologies.orient.etl.OETLProcessHaltedException;
import com.orientechnologies.orient.etl.OETLProcessor;
import com.tinkerpop.blueprints.impls.orient.OrientEdge;
import com.tinkerpop.blueprints.impls.orient.OrientEdgeType;
import com.tinkerpop.blueprints.impls.orient.OrientVertex;

import java.util.ArrayList;
import java.util.List;

public class OEdgeTransformer extends OAbstractLookupTransformer {
  private String    edgeClass      = OrientEdgeType.CLASS_NAME;
  private boolean   directionOut   = true;
  private ODocument targetVertexFields;
  private ODocument edgeFields;
  private boolean   skipDuplicates = false;

  @Override
  public ODocument getConfiguration() {
    return new ODocument()
        .fromJSON("{parameters:["
            + getCommonConfigurationParameters()
            + ","
            + "{joinValue:{optional:true,description:'value to use for join'}},"
            + "{joinFieldName:{optional:true,description:'field name containing the value to join'}},"
            + "{lookup:{optional:false,description:'<Class>.<property> or Query to execute'}},"
            + "{direction:{optional:true,description:'Direction between \'in\' and \'out\'. Default is \'out\''}},"
            + "{class:{optional:true,description:'Edge class name. Default is \'E\''}},"
            + "{targetVertexFields:{optional:true,description:'Map of fields to set in target vertex. Use ${$input.<field>} to get input field values'}},"
            + "{edgeFields:{optional:true,description:'Map of fields to set in edge. Use ${$input.<field>} to get input field values'}},"
            + "{skipDuplicates:{optional:true,description:'Duplicated edges (with a composite index built on both out and in properties) are skipped', default:false}},"
            + "{unresolvedVertexAction:{optional:true,description:'action when a unresolved vertices is found',values:"
            + stringArray2Json(ACTION.values()) + "}}]," + "input:['ODocument','OrientVertex'],output:'OrientVertex'}");
  }

  @Override
  public void configure(OETLProcessor iProcessor, final ODocument iConfiguration, final OCommandContext iContext) {
    super.configure(iProcessor, iConfiguration, iContext);
    edgeClass = iConfiguration.field("class");
    if (iConfiguration.containsField("direction")) {
      final String direction = iConfiguration.field("direction");
      if ("out".equalsIgnoreCase(direction))
        directionOut = true;
      else if ("in".equalsIgnoreCase(direction))
        directionOut = false;
      else
        throw new OConfigurationException("Direction can be 'in' or 'out', but found: " + direction);
    }

    if (iConfiguration.containsField("targetVertexFields"))
      targetVertexFields = (ODocument) iConfiguration.field("targetVertexFields");
    if (iConfiguration.containsField("edgeFields"))
      edgeFields = (ODocument) iConfiguration.field("edgeFields");
    if (iConfiguration.containsField("skipDuplicates"))
      skipDuplicates = (Boolean) resolve(iConfiguration.field("skipDuplicates"));
  }

  @Override
  public String getName() {
    return "edge";
  }

  @Override
  public void begin() {
    final OClass cls = pipeline.getGraphDatabase().getEdgeType(edgeClass);
    if (cls == null)
      pipeline.getGraphDatabase().createEdgeType(edgeClass);
    super.begin();
  }

  @Override
  public Object executeTransform(final Object input) {
    for (Object o : OMultiValue.getMultiValueIterable(input)) {
      // GET JOIN VALUE
      final OrientVertex vertex;
      if (o instanceof OrientVertex)
        vertex = (OrientVertex) o;
      else if (o instanceof OIdentifiable)
        vertex = pipeline.getGraphDatabase().getVertex(o);
      else
        throw new OTransformException(getName() + ": input type '" + o + "' is not supported");

      final Object joinCurrentValue = joinValue != null ? joinValue : vertex.getProperty(joinFieldName);

      if (OMultiValue.isMultiValue(joinCurrentValue)) {
        // RESOLVE SINGLE JOINS
        for (Object ob : OMultiValue.getMultiValueIterable(joinCurrentValue)) {
          final Object r = lookup(ob, true);
          if (createEdge(vertex, ob, r) == null) {
            if (unresolvedLinkAction == ACTION.SKIP)
              // RETURN NULL ONLY IN CASE SKIP ACTION IS REQUESTED
              return null;
          }
        }
      } else {
        final Object result = lookup(joinCurrentValue, true);
        if (createEdge(vertex, joinCurrentValue, result) == null) {
          if (unresolvedLinkAction == ACTION.SKIP)
            // RETURN NULL ONLY IN CASE SKIP ACTION IS REQUESTED
            return null;
        }
      }
    }

    return input;
  }

  private List<OrientEdge> createEdge(final OrientVertex vertex, final Object joinCurrentValue, Object result) {
    log(OETLProcessor.LOG_LEVELS.DEBUG, "joinCurrentValue=%s, lookupResult=%s", joinCurrentValue, result);

    if (result == null) {
      // APPLY THE STRATEGY DEFINED IN unresolvedLinkAction
      switch (unresolvedLinkAction) {
      case CREATE:
        // Don't try to create a Vertex with a null value
        if (joinCurrentValue != null) {
          if (lookup != null) {
            final String[] lookupParts = lookup.split("\\.");
            final OrientVertex linkedV = pipeline.getGraphDatabase().addTemporaryVertex(lookupParts[0]);
            linkedV.setProperty(lookupParts[1], joinCurrentValue);

            if (targetVertexFields != null) {
              for (String f : targetVertexFields.fieldNames())
                linkedV.setProperty(f, resolve(targetVertexFields.field(f)));
            }

            linkedV.save();

            log(OETLProcessor.LOG_LEVELS.DEBUG, "created new vertex=%s", linkedV.getRecord());

            result = linkedV.getIdentity();
          } else {
            throw new OConfigurationException("Cannot create linked document because target class is unknown. Use 'lookup' field");
          }
        }
        break;
      case ERROR:
        processor.getStats().incrementErrors();
        log(OETLProcessor.LOG_LEVELS.ERROR, "%s: ERROR Cannot resolve join for value '%s'", getName(), joinCurrentValue);
        break;
      case WARNING:
        processor.getStats().incrementWarnings();
        log(OETLProcessor.LOG_LEVELS.INFO, "%s: WARN Cannot resolve join for value '%s'", getName(), joinCurrentValue);
        break;
      case SKIP:
        return null;
      case HALT:
        throw new OETLProcessHaltedException("Cannot resolve join for value '" + joinCurrentValue + "'");
      case NOTHING:
      default:
        return null;
      }
    }

    if (result != null) {
      final List<OrientEdge> edges;
      if (OMultiValue.isMultiValue(result)) {
        final int size = OMultiValue.getSize(result);
        if (size == 0)
          // NO EDGES
          return null;

        edges = new ArrayList<OrientEdge>(size);
      } else
        edges = new ArrayList<OrientEdge>(1);

      for (Object o : OMultiValue.getMultiValueIterable(result)) {
        OIdentifiable oid = (OIdentifiable) o;
        final OrientVertex targetVertex = pipeline.getGraphDatabase().getVertex(oid);

        try {
          // CREATE THE EDGE
          final OrientEdge edge;
          if (directionOut)
            edge = (OrientEdge) vertex.addEdge(edgeClass, targetVertex);
          else
            edge = (OrientEdge) targetVertex.addEdge(edgeClass, vertex);

          if (edgeFields != null) {
            for (String f : edgeFields.fieldNames())
              edge.setProperty(f, resolve(edgeFields.field(f)));
          }

          edges.add(edge);
          log(OETLProcessor.LOG_LEVELS.DEBUG, "created new edge=%s", edge);
        } catch (ORecordDuplicatedException e) {
          if (skipDuplicates) {
            log(OETLProcessor.LOG_LEVELS.DEBUG, "skipped creation of new edge because already exists");
            continue;
          } else {
            log(OETLProcessor.LOG_LEVELS.ERROR, "error on creation of new edge because it already exists (skipDuplicates=false)");
            throw e;
          }
        }
      }

      return edges;
    }

    // NO EDGES
    return null;
  }
}