// Copyright 2013-2018 Workiva Inc.
//
// Licensed under the Boost Software License (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.boost.org/LICENSE_1_0.txt
//
// 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.
//
// This software or document includes material copied from or derived
// from JSON-Schema-Test-Suite (https://github.com/json-schema-org/JSON-Schema-Test-Suite),
// Copyright (c) 2012 Julian Berman, which is licensed under the following terms:
//
//     Copyright (c) 2012 Julian Berman
//
//     Permission is hereby granted, free of charge, to any person obtaining a copy
//     of this software and associated documentation files (the "Software"), to deal
//     in the Software without restriction, including without limitation the rights
//     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//     copies of the Software, and to permit persons to whom the Software is
//     furnished to do so, subject to the following conditions:
//
//     The above copyright notice and this permission notice shall be included in
//     all copies or substantial portions of the Software.
//
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//     THE SOFTWARE.

/// Functionality to create Graphviz input dot file from schema
library json_schema.schema_dot;

import 'package:json_schema/json_schema.dart';

// custom <additional imports>
// end <additional imports>

/// Represents one node in the schema diagram
class SchemaNode {
  /// Referenced schema this node portrays
  Schema schema;

  /// List of links (resulting in graph edge) from this node to another
  List<String> links;

  // custom <class SchemaNode>

  SchemaNode(Schema this.schema, [this.links]) {
    if (links == null) links = [];
  }

  static bool schemaShown(Schema schema) =>
      schema.properties.length > 0 ||
      schema.definitions.length > 0 ||
      schema.anyOf.length > 0 ||
      schema.oneOf.length > 0 ||
      schema.allOf.length > 0 ||
      schema.enumValues.length > 0 ||
      schema.additionalPropertiesSchema != null ||
      schema.minimum != null ||
      schema.maximum != null ||
      schema.ref != null;

  String get nodes {
    List lines = schema.refMap.values
        .where((schema) => schemaShown(schema))
        .map((schema) => new SchemaNode(schema, links).node)
        .toList();
    lines.addAll(links.map((link) => "$link;"));
    return lines.join('\n');
  }

  String get node {
    var data = ['"${schema.path}" [']..add(label.join('\n'))..add(']');
    return data.join('\n');
  }

  static dynamic schemaType(Schema schema) {
    dynamic result;
    var schemaTypeList = schema.schemaTypeList;
    if (schemaTypeList == null) {
      if (schema.oneOf.length > 0) {
        result = "oneOf:${schema.oneOf.map((schema) => schemaType(schema)).toList()}";
      } else if (schema.anyOf.length > 0) {
        result = "anyOf:${schema.anyOf.map((schema) => schemaType(schema)).toList()}";
      } else if (schema.allOf.length > 0) {
        result = "allOf:${schema.allOf.map((schema) => schemaType(schema)).toList()}";
      } else if (schema.defaultValue != null) {
        result = "default=${schema.defaultValue}";
      } else if (schema.ref != null) {
        result = "ref=${schema.ref}";
      } else if (schema.enumValues != null && schema.enumValues.length > 0) {
        result = "enum=${schema.enumValues}";
      } else if (schema.schemaMap.length == 0) {
        result = "{}";
      } else {
        result = "$schema";
      }
    } else {
      result = schemaTypeList.length == 1 ? schemaTypeList[0] : schemaTypeList;
    }
    if ((result is List) && result.length == 0) result = {};
    if (result == null) result = {};
    return result;
  }

  List<String> get label {
    return ['label =<']
      ..add('<table border="0" cellborder="0" cellpadding="1" bgcolor="white">')
      ..add(wrap(schema.path, port: "@path"))
      ..add(wrap(title))
      ..add(wrap(description))
      ..addAll(definitionEntries)
      ..addAll(propertyEntries)
      ..addAll(additionalPropertiesSchema)
      ..addAll(propertyDependencies)
      ..addAll(schemaDependencies)
      ..addAll(minimum)
      ..addAll(maximum)
      ..addAll(defaultValue)
      ..addAll(anyOf)
      ..addAll(oneOf)
      ..addAll(allOf)
      ..addAll(enumEntries)
      ..add('</table>')
      ..add('>');
  }

  List<String> get defaultValue {
    List<String> result = [];
    if (schema.defaultValue != null) {
      result.add(wrapRowDistinct('default', "${schema.defaultValue}"));
    }
    return result;
  }

  List<String> get minimum {
    List<String> result = [];
    if (schema.minimum != null) {
      result.add(wrapRowDistinct('minimum', "${schema.minimum}"));
    }
    return result;
  }

  List<String> get maximum {
    List<String> result = [];
    if (schema.maximum != null) {
      result.add(wrapRowDistinct('maximum', "${schema.maximum}"));
    }
    return result;
  }

  List<String> get multipleOf {
    List<String> result = [];
    if (schema.multipleOf != null) {
      result.add(wrapRowDistinct('multipleOf', "${schema.multipleOf}"));
    }
    return result;
  }

  List<String> get enumEntries {
    List<String> enumValues = [];
    if (schema.enumValues.length > 0) {
      enumValues.add(wrap('Enum Values', color: 'beige'));
      schema.enumValues.forEach((value) {
        enumValues.add(wrap("$value", color: 'grey'));
      });
    }
    return enumValues;
  }

  List<String> get anyOf {
    List<String> anyOf = [];
    if (schema.anyOf.length > 0) {
      anyOf.add(wrap('Any Of', color: 'beige'));
      int i = 0;
      schema.anyOf.forEach((anyOfSchema) {
        String port = "${i++}";
        makeSchemaLink(port, schema, anyOfSchema);
        anyOf.add(wrap(abbreviatedString("${schemaType(anyOfSchema)}", 30), color: 'grey', port: port));
      });
    }
    return anyOf;
  }

  List<String> get oneOf {
    List<String> oneOf = [];
    if (schema.oneOf.length > 0) {
      oneOf.add(wrap('One Of', color: 'beige'));
      int i = 0;
      schema.oneOf.forEach((oneOfSchema) {
        String port = "${i++}";
        makeSchemaLink(port, schema, oneOfSchema);
        oneOf.add(wrap(abbreviatedString("${schemaType(oneOfSchema)}", 30), color: 'grey', port: port));
      });
    }
    return oneOf;
  }

  List<String> get allOf {
    List<String> allOf = [];
    if (schema.allOf.length > 0) {
      allOf.add(wrap('All Of', color: 'beige'));
      int i = 0;
      schema.allOf.forEach((allOfSchema) {
        String port = "${i++}";
        makeSchemaLink(port, schema, allOfSchema);
        allOf.add(wrap(abbreviatedString("${schemaType(allOfSchema)}", 30), color: 'grey', port: port));
      });
    }
    return allOf;
  }

  List<String> get propertyDependencies {
    List<String> result = [];
    if (schema.propertyDependencies.length > 0) {
      result.add(wrap('Property Dependencies'));
      schema.propertyDependencies.forEach((key, val) {
        result.add(wrapRowDistinct(key, val.toString()));
      });
    }
    return result;
  }

  List<String> get schemaDependencies {
    List<String> result = [];
    if (schema.schemaDependencies.length > 0) {
      result.add('Property Dependencies');
      schema.propertyDependencies.forEach((key, val) {
        result.add(wrapRowDistinct(key, val.toString()));
      });
    }
    return result;
  }

  makeSchemaPort(String port, Schema schema) => '"${schema.path}":"$port"';

  makeSchemaLink(String port, Schema src, Schema target) {
    if (schemaShown(target)) links.add('${makeSchemaPort(port, src)} -> ${makeSchemaPort("@path", target)}');
  }

  List<String> get additionalPropertiesSchema {
    List<String> result = [];
    Schema other = schema.additionalPropertiesSchema;
    if (other != null) {
      result.add(wrap('Additional Properties', color: 'lemonchiffon'));
      String port = "mustBe";
      makeSchemaLink(port, schema, other);
      result.add(wrapRowDistinct('Must Be: ', abbreviatedString(schemaType(other).toString(), 30), port));
    }
    return result;
  }

  List<String> get propertyEntries {
    List<String> props = [];
    if (schema.properties.length > 0) {
      props.add(wrap('Properties'));
      var sortedProps = new List.from(schema.properties.keys)..sort();
      sortedProps.forEach((prop) {
        var propertySchema = schema.properties[prop];
        String requiredPrefix = schema.propertyRequired(prop) ? '! ' : '? ';
        String port = "@$prop";
        if (schemaShown(propertySchema)) {
          makeSchemaLink(port, schema, propertySchema);
        } else if (propertySchema.items is Schema && schemaShown(propertySchema.items)) {
          makeSchemaLink(port, schema, propertySchema.items);
        }
        props.add(wrapRowDistinct(
            "$requiredPrefix$prop", abbreviatedString(schemaType(propertySchema).toString(), 30), port));
      });
    }
    return props;
  }

  List<String> get definitionEntries {
    List<String> definitions = [];
    if (schema.definitions.length > 0) {
      definitions
          .add('<tr><td bgcolor="wheat" align="center" colspan="2"><font color="black">Definitions</font></td></tr>');
      var sortedDefinitions = new List.from(schema.definitions.keys)..sort();
      sortedDefinitions.forEach((key) {
        definitions.add(wrapRowDistinct(key, '', "${schema.path}@$key"));
      });
    }
    return definitions;
  }

  String wrap(String s, {String port: '', String color: 'wheat'}) => s == null
      ? ''
      : '<tr><td bgcolor="$color" align="center" colspan="2" port="$port"><font color="black">$s</font></td></tr>';

  String wrapRowDistinct(String first, String second, [String port = '']) =>
      '<tr><td align="left" port="$port">$first</td>$first<td bgcolor="grey" align="right">$second</td></tr>';

  String get title => schema.title != null ? abbreviatedString("title=${schema.title}", 30) : null;

  String get description => schema.description != null ? abbreviatedString("descr=${schema.description}", 30) : null;

  String abbreviatedString(String s, [int len = 15]) {
    if (s == null) return s;
    len -= 3;
    if (len >= s.length) {
      return s;
    } else {
      return s.substring(0, len) + '...';
    }
  }

  // end <class SchemaNode>

}

// custom <library schema_dot>

/// Return a dot specification for [schema]
String createDot(Schema schema) => '''
digraph G {
  fontname = "Bitstream Vera Sans"
  fontsize = 8

  node [
    fontname = "Courier"
    fontsize = 8
    shape = "plaintext"
  ]

  edge [
    fontname = "Bitstream Vera Sans"
    fontsize = 8
  ]

${new SchemaNode(schema).nodes}


}

''';

// end <library schema_dot>
