/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 *
 *   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 org.apache.streams.util.schema;

import org.apache.streams.util.PropertyUtil;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Ordering;
import org.apache.commons.lang3.StringUtils;
import org.jsonschema2pojo.ContentResolver;
import org.jsonschema2pojo.FragmentResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static org.apache.streams.util.schema.UriUtil.safeResolve;

/**
 * Default Implementation of SchemaStore.
 */
public class SchemaStoreImpl extends Ordering<Schema> implements SchemaStore {

  private static final Logger LOGGER = LoggerFactory.getLogger(SchemaStore.class);
  private static final JsonNodeFactory NODE_FACTORY = JsonNodeFactory.instance;

  protected Map<URI, Schema> schemas = new HashMap<>();
  protected FragmentResolver fragmentResolver = new FragmentResolver();
  protected ContentResolver contentResolver = new ContentResolver();

  public SchemaStoreImpl() {
  }

  @Override
  public synchronized Schema create(URI uri) {
    if (!getByUri(uri).isPresent()) {
      URI baseUri = UriUtil.removeFragment(uri);
      JsonNode baseNode = this.contentResolver.resolve(baseUri);
      if (uri.toString().contains("#") && !uri.toString().endsWith("#")) {
        Schema newSchema = new Schema(baseUri, baseNode, null, true);
        this.schemas.put(baseUri, newSchema);
        JsonNode childContent = this.fragmentResolver.resolve(baseNode, '#' + StringUtils.substringAfter(uri.toString(), "#"), "#");
        this.schemas.put(uri, new Schema(uri, childContent, newSchema, false));
      } else {
        if ( baseNode.has("extends") && baseNode.get("extends").isObject()) {
          URI ref = URI.create((baseNode.get("extends")).get("$ref").asText());
          URI absoluteUri;
          if ( ref.isAbsolute()) {
            absoluteUri = ref;
          } else {
            absoluteUri = baseUri.resolve(ref);
          }
          JsonNode parentNode = this.contentResolver.resolve(absoluteUri);
          Schema parentSchema;
          if ( this.schemas.get(absoluteUri) != null ) {
            parentSchema = this.schemas.get(absoluteUri);
          } else {
            parentSchema = create(absoluteUri);
          }
          this.schemas.put(uri, new Schema(uri, baseNode, parentSchema, true));
        } else {
          this.schemas.put(uri, new Schema(uri, baseNode, null, true));
        }
      }
      List<JsonNode> refs = baseNode.findValues("$ref");
      for ( JsonNode ref : refs ) {
        if ( ref.isValueNode() ) {
          String refVal = ref.asText();
          URI refUri = null;
          try {
            refUri = URI.create(refVal);
          } catch ( Exception ex ) {
            LOGGER.info("Exception: {}", ex.getMessage());
          }
          if (refUri != null && !getByUri(refUri).isPresent()) {
            if (refUri.isAbsolute()) {
              create(refUri);
            } else {
              create(baseUri.resolve(refUri));
            }
          }
        }
      }
    }

    return this.schemas.get(uri);
  }

  @Override
  public Schema create(Schema parent, String path) {
    if (path.equals("#")) {
      return parent;
    } else {
      path = StringUtils.stripEnd(path, "#?&/");
      URI id = (parent != null && parent.getId() != null)
          ? parent.getId().resolve(path)
          : URI.create(path);
      if (this.selfReferenceWithoutParentFile(parent, path)) {
        Schema newSchema = new Schema(id, this.fragmentResolver.resolve(parent.getParentContent(), path, "#"), parent, false);
        this.schemas.put(id, newSchema);
        return this.schemas.get(id);
      } else {
        return this.create(id);
      }
    }
  }

  protected boolean selfReferenceWithoutParentFile(Schema parent, String path) {
    return parent != null && (parent.getId() == null || parent.getId().toString().startsWith("#/")) && path.startsWith("#/");
  }

  @Override
  public synchronized void clearCache() {
    this.schemas.clear();
  }

  @Override
  public Integer getSize() {
    return schemas.size();
  }

  @Override
  public Optional<Schema> getById(URI id) {
    for ( Schema schema : schemas.values() ) {
      if ( schema.getId() != null && schema.getId().equals(id) ) {
        return Optional.of(schema);
      }
    }
    return Optional.empty();
  }

  @Override
  public Optional<Schema> getByUri(URI uri) {
    for ( Schema schema : schemas.values() ) {
      if ( schema.getUri().equals(uri) ) {
        return Optional.of(schema);
      }
    }
    return Optional.empty();
  }

  @Override
  public Integer getFileUriCount() {
    int count = 0;
    for ( Schema schema : schemas.values() ) {
      if ( schema.getUri().getScheme().equals("file") ) {
        count++;
      }
    }
    return count;
  }

  @Override
  public Integer getHttpUriCount() {
    int count = 0;
    for ( Schema schema : schemas.values() ) {
      if ( schema.getUri().getScheme().equals("http") ) {
        count++;
      }
    }
    return count;
  }

  @Override
  public Iterator<Schema> getSchemaIterator() {
    List<Schema> schemaList = new ArrayList<>(schemas.values());
    schemaList.sort(this);
    return schemaList.iterator();
  }

  @Override
  public ObjectNode resolveProperties(Schema schema, ObjectNode fieldNode, String resourceId) {
    // this should return something more suitable like:
    //   Map<String, Pair<Schema, ObjectNode>>
    ObjectNode schemaProperties = NODE_FACTORY.objectNode();
    ObjectNode parentProperties = NODE_FACTORY.objectNode();
    if (fieldNode == null) {
      ObjectNode schemaContent = (ObjectNode) schema.getContent();
      if (schemaContent.has("properties")) {
        schemaProperties = (ObjectNode) schemaContent.get("properties");
        if (schema.getParentContent() != null) {
          ObjectNode parentContent = (ObjectNode) schema.getParentContent();
          if (parentContent.has("properties")) {
            parentProperties = (ObjectNode) parentContent.get("properties");
          }
        }
      }
    } else if (fieldNode.size() > 0) {
      if (fieldNode.has("properties") && fieldNode.get("properties").isObject() && fieldNode.get("properties").size() > 0) {
        schemaProperties = (ObjectNode) fieldNode.get("properties");
      }
      URI parentUri = null;
      if ( fieldNode.has("$ref") || fieldNode.has("extends") ) {
        JsonNode refNode = fieldNode.get("$ref");
        JsonNode extendsNode = fieldNode.get("extends");
        if (refNode != null && refNode.isValueNode()) {
          parentUri = URI.create(refNode.asText());
        } else if (extendsNode != null && extendsNode.isObject()) {
          parentUri = URI.create(extendsNode.get("$ref").asText());
        }
        ObjectNode parentContent = null;
        URI absoluteUri;
        if (parentUri.isAbsolute()) {
          absoluteUri = parentUri;
        } else {
          absoluteUri = schema.getUri().resolve(parentUri);
          if (!absoluteUri.isAbsolute() || (absoluteUri.isAbsolute() && !getByUri(absoluteUri).isPresent() )) {
            absoluteUri = schema.getParentUri().resolve(parentUri);
          }
        }
        if (absoluteUri.isAbsolute()) {
          if (getByUri(absoluteUri).isPresent()) {
            parentContent = (ObjectNode) getByUri(absoluteUri).get().getContent();
          }
          if (parentContent != null && parentContent.isObject() && parentContent.has("properties")) {
            parentProperties = (ObjectNode) parentContent.get("properties");
          } else if (absoluteUri.getPath().endsWith("#properties")) {
            absoluteUri = URI.create(absoluteUri.toString().replace("#properties", ""));
            parentProperties = (ObjectNode) getByUri(absoluteUri).get().getContent().get("properties");
          }
        }
      }


    }

    ObjectNode resolvedProperties = NODE_FACTORY.objectNode();
    if (parentProperties != null && parentProperties.size() > 0) {
      resolvedProperties = PropertyUtil.mergeProperties(schemaProperties, parentProperties);
    } else {
      resolvedProperties = schemaProperties.deepCopy();
    }

    return resolvedProperties;
  }

  /**
   * resolve full definition of 'items'.
   * @param schema Schema
   * @param fieldNode ObjectNode
   * @param resourceId resourceId
   * @return ObjectNode
   */
  public ObjectNode resolveItems(Schema schema, ObjectNode fieldNode, String resourceId) {
    ObjectNode schemaItems = NODE_FACTORY.objectNode();
    ObjectNode parentItems = NODE_FACTORY.objectNode();
    if (fieldNode == null) {
      ObjectNode schemaContent = (ObjectNode) schema.getContent();
      if ( schemaContent.has("items") ) {
        schemaItems = (ObjectNode) schemaContent.get("items");
        if (schema.getParentContent() != null) {
          ObjectNode parentContent = (ObjectNode) schema.getParentContent();
          if (parentContent.has("items")) {
            parentItems = (ObjectNode) parentContent.get("items");
          }
        }
      }
    } else if (fieldNode.size() > 0) {
      if (fieldNode.has("items") && fieldNode.get("items").isObject() && fieldNode.get("items").size() > 0) {
        schemaItems = (ObjectNode) fieldNode.get("items");
      }
      URI parentUri = null;
      if ( fieldNode.has("$ref") || fieldNode.has("extends") ) {
        JsonNode refNode = fieldNode.get("$ref");
        JsonNode extendsNode = fieldNode.get("extends");
        if (refNode != null && refNode.isValueNode()) {
          parentUri = URI.create(refNode.asText());
        } else if (extendsNode != null && extendsNode.isObject()) {
          parentUri = URI.create(extendsNode.get("$ref").asText());
        }
        ObjectNode parentContent = null;
        URI absoluteUri;
        if (parentUri.isAbsolute()) {
          absoluteUri = parentUri;
        } else {
          absoluteUri = schema.getUri().resolve(parentUri);
          if (!absoluteUri.isAbsolute() || (absoluteUri.isAbsolute() && !getByUri(absoluteUri).isPresent() )) {
            absoluteUri = schema.getParentUri().resolve(parentUri);
          }
        }
        if (absoluteUri.isAbsolute()) {
          if (getByUri(absoluteUri).isPresent()) {
            parentContent = (ObjectNode) getByUri(absoluteUri).get().getContent();
          }
          if (parentContent != null && parentContent.isObject() && parentContent.has("items")) {
            parentItems = (ObjectNode) parentContent.get("items");
          } else if (absoluteUri.getPath().endsWith("#items")) {
            absoluteUri = URI.create(absoluteUri.toString().replace("#items", ""));
            parentItems = (ObjectNode) getByUri(absoluteUri).get().getContent().get("items");
          }
        }
      }
    }

    ObjectNode resolvedItems = NODE_FACTORY.objectNode();
    if (parentItems != null && parentItems.size() > 0) {
      resolvedItems = PropertyUtil.mergeProperties(schemaItems, parentItems);
    } else {
      resolvedItems = schemaItems.deepCopy();
    }

    return resolvedItems;
  }

  @Override
  public int compare(Schema left, Schema right) {
    // are they the same?
    if ( left.equals(right)) {
      return 0;
    }
    // is one an ancestor of the other
    Schema candidateAncestor = left;
    while ( candidateAncestor.getParent() != null ) {
      candidateAncestor = candidateAncestor.getParent();
      if ( candidateAncestor.equals(right)) {
        return 1;
      }
    }
    candidateAncestor = right;
    while ( candidateAncestor.getParent() != null ) {
      candidateAncestor = candidateAncestor.getParent();
      if ( candidateAncestor.equals(left)) {
        return -1;
      }
    }
    // does one have a field that reference the other?
    for ( JsonNode refNode : left.getContent().findValues("$ref") ) {
      String refText = refNode.asText();
      Optional<URI> resolvedUri = safeResolve(left.getUri(), refText);
      if ( resolvedUri.isPresent() && resolvedUri.get().equals(right.getUri())) {
        return 1;
      }
    }
    for ( JsonNode refNode : right.getContent().findValues("$ref") ) {
      String refText = refNode.asText();
      Optional<URI> resolvedUri = safeResolve(right.getUri(), refText);
      if ( resolvedUri.isPresent() && resolvedUri.get().equals(left.getUri())) {
        return -1;
      }
    }
    // does one have a field that reference a third schema that references the other?
    for ( JsonNode refNode : left.getContent().findValues("$ref") ) {
      String refText = refNode.asText();
      Optional<URI> possibleConnectorUri = safeResolve(left.getUri(), refText);
      if ( possibleConnectorUri.isPresent()) {
        Optional<Schema> possibleConnector = getByUri(possibleConnectorUri.get());
        if (possibleConnector.isPresent()) {
          for (JsonNode connectorRefNode : possibleConnector.get().getContent().findValues("$ref")) {
            String connectorRefText = connectorRefNode.asText();
            Optional<URI> resolvedUri = safeResolve(possibleConnector.get().getUri(), connectorRefText);
            if (resolvedUri.isPresent() && resolvedUri.get().equals(right.getUri())) {
              return 1;
            }
          }
        }
      }
    }
    for ( JsonNode refNode : right.getContent().findValues("$ref") ) {
      String refText = refNode.asText();
      Optional<URI> possibleConnectorUri = safeResolve(right.getUri(), refText);
      if ( possibleConnectorUri.isPresent()) {
        Optional<Schema> possibleConnector = getByUri(possibleConnectorUri.get());
        if (possibleConnector.isPresent()) {
          for (JsonNode connectorRefNode : possibleConnector.get().getContent().findValues("$ref")) {
            String connectorRefText = connectorRefNode.asText();
            Optional<URI> resolvedUri = safeResolve(possibleConnector.get().getUri(), connectorRefText);
            if (resolvedUri.isPresent() && resolvedUri.get().equals(left.getUri())) {
              return -1;
            }
          }
        }
      }
    }
    // there still has to be some order even when there are no connections.
    // we'll arbitrarily pick alphabetic by ID
    int lexigraphic = right.toString().compareTo(left.toString());
    return ( lexigraphic / Math.abs(lexigraphic) );
  }

}

