// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';

PSEntry _findEntry(YamlMap map, String key) {
  PSEntry entry;
  map.nodes.forEach((k, v) {
    if (k is YamlScalar && key == k.toString()) {
      entry = _processScalar(k, v);
    }
  });
  return entry;
}

PSDependencyList _processDependencies(YamlScalar key, YamlNode v) {
  if (v is! YamlMap) {
    return null;
  }
  YamlMap depsMap = v;

  _PSDependencyList deps = new _PSDependencyList(new _PSNode(key));
  depsMap.nodes.forEach((k, v) => deps.add(new _PSDependency(k, v)));
  return deps;
}

PSGitRepo _processGitRepo(YamlScalar key, YamlNode v) {
  if (v is! YamlMap) {
    return null;
  }
  YamlMap hostMap = v;
  // url: git://github.com/munificent/kittens.git
  // ref: some-branch
  _PSGitRepo repo = new _PSGitRepo();
  repo.token = new _PSNode(key);
  repo.ref = _findEntry(hostMap, 'ref');
  repo.url = _findEntry(hostMap, 'url');
  return repo;
}

PSHost _processHost(YamlScalar key, YamlNode v) {
  if (v is! YamlMap) {
    return null;
  }
  YamlMap hostMap = v;
  // name: transmogrify
  // url: http://your-package-server.com
  _PSHost host = new _PSHost();
  host.token = new _PSNode(key);
  host.name = _findEntry(hostMap, 'name');
  host.url = _findEntry(hostMap, 'url');
  return host;
}

PSNodeList _processList(YamlScalar key, YamlNode v) {
  if (v is! YamlList) {
    return null;
  }
  YamlList nodeList = v;

  return new _PSNodeList(
      new _PSNode(key), nodeList.nodes.map((n) => new _PSNode(n)));
}

PSEntry _processScalar(YamlScalar key, YamlNode value) {
  if (value is! YamlScalar) {
    return null;
    //WARN?
  }
  return new PSEntry(new _PSNode(key), new _PSNode(value));
}

abstract class PSDependency {
  PSGitRepo get git;
  PSHost get host;
  PSNode get name;
  PSEntry get version;
}

abstract class PSDependencyList extends Object
    with IterableMixin<PSDependency> {}

class PSEntry {
  final PSNode key;
  final PSNode value;
  PSEntry(this.key, this.value);

  @override
  String toString() => '${key != null ? (key.toString() + ': ') : ''}$value';
}

abstract class PSGitRepo {
  PSEntry get ref;
  PSNode get token;
  PSEntry get url;
}

abstract class PSHost {
  PSEntry get name;
  PSNode get token;
  PSEntry get url;
}

abstract class PSNode {
  SourceSpan get span;
  String get text;
}

abstract class PSNodeList extends Object with IterableMixin<PSNode> {
  @override
  Iterator<PSNode> get iterator;
  PSNode get token;
}

abstract class Pubspec {
  factory Pubspec.parse(String source, {Uri sourceUrl}) =>
      new _Pubspec(source, sourceUrl: sourceUrl);
  PSEntry get author;
  PSNodeList get authors;
  PSDependencyList get dependencies;
  PSDependencyList get dependencyOverrides;
  PSEntry get description;
  PSDependencyList get devDependencies;
  PSEntry get documentation;
  PSEntry get homepage;
  PSEntry get name;
  PSEntry get version;
  accept(PubspecVisitor visitor);
}

abstract class PubspecVisitor<T> {
  T visitPackageAuthor(PSEntry author) => null;
  T visitPackageAuthors(PSNodeList authors) => null;
  T visitPackageDependencies(PSDependencyList dependencies) => null;
  T visitPackageDependency(PSDependency dependency) => null;
  T visitPackageDependencyOverrides(PSDependencyList dependencies) => null;
  T visitPackageDependencyOverride(PSDependency dependency) => null;
  T visitPackageDescription(PSEntry description) => null;
  T visitPackageDevDependencies(PSDependencyList dependencies) => null;
  T visitPackageDevDependency(PSDependency dependency) => null;
  T visitPackageDocumentation(PSEntry documentation) => null;
  T visitPackageHomepage(PSEntry homepage) => null;
  T visitPackageName(PSEntry name) => null;
  T visitPackageVersion(PSEntry version) => null;
}

class _PSDependency extends PSDependency {
  @override
  PSNode name;
  @override
  PSEntry version;
  @override
  PSHost host;
  @override
  PSGitRepo git;

  factory _PSDependency(dynamic k, YamlNode v) {
    if (k is! YamlScalar) {
      return null;
    }
    YamlScalar key = k;

    _PSDependency dep = new _PSDependency._();

    dep.name = new _PSNode(key);

    if (v is YamlScalar) {
      // Simple version
      dep.version = new PSEntry(null, new _PSNode(v));
    } else if (v is YamlMap) {
      // hosted:
      //   name: transmogrify
      //   url: http://your-package-server.com
      //   version: '>=0.4.0 <1.0.0'
      YamlMap details = v;
      details.nodes.forEach((k, v) {
        if (k is! YamlScalar) {
          return;
        }
        YamlScalar key = k;
        switch (key.toString()) {
          case 'version':
            dep.version = _processScalar(key, v);
            break;
          case 'hosted':
            dep.host = _processHost(key, v);
            break;
          case 'git':
            dep.git = _processGitRepo(key, v);
            break;
        }
      });
    }
    return dep;
  }

  _PSDependency._();

  @override
  String toString() {
    var sb = new StringBuffer();
    if (name != null) {
      sb.write('$name:');
    }
    var versionInfo = '';
    if (version != null) {
      if (version.key == null) {
        versionInfo = ' $version';
      } else {
        versionInfo = '\n    $version';
      }
    }
    sb.writeln(versionInfo);
    if (host != null) {
      sb.writeln(host);
    }
    if (git != null) {
      sb.writeln(git);
    }
    return sb.toString();
  }
}

class _PSDependencyList extends PSDependencyList {
  final dependencies = <PSDependency>[];
  final PSNode token;

  _PSDependencyList(this.token);

  @override
  Iterator<PSDependency> get iterator => dependencies.iterator;

  add(PSDependency dependency) {
    if (dependency != null) {
      dependencies.add(dependency);
    }
  }

  @override
  String toString() => '$token\n${dependencies.join('  ')}';
}

class _PSGitRepo implements PSGitRepo {
  @override
  PSNode token;
  @override
  PSEntry ref;
  @override
  PSEntry url;
  @override
  String toString() => '''
    $token:
      $url
      $ref''';
}

class _PSHost implements PSHost {
  @override
  PSNode token;
  @override
  PSEntry name;
  @override
  PSEntry url;
  @override
  String toString() => '''
    $token:
      $name
      $url''';
}

class _PSNode implements PSNode {
  @override
  final String text;
  @override
  final SourceSpan span;

  _PSNode(YamlNode node)
      : text = node.value?.toString(),
        span = node.span;

  @override
  String toString() => '$text';
}

class _PSNodeList extends PSNodeList {
  @override
  final PSNode token;
  final Iterable<PSNode> nodes;

  _PSNodeList(this.token, this.nodes);

  @override
  Iterator<PSNode> get iterator => nodes.iterator;

  @override
  String toString() => '''
$token:
  - ${nodes.join('\n  - ')}''';
}

class _Pubspec implements Pubspec {
  @override
  PSEntry author;
  @override
  PSNodeList authors;
  @override
  PSEntry description;
  @override
  PSEntry documentation;
  @override
  PSEntry homepage;
  @override
  PSEntry name;
  @override
  PSEntry version;
  @override
  PSDependencyList dependencies;
  @override
  PSDependencyList devDependencies;
  @override
  PSDependencyList dependencyOverrides;

  _Pubspec(String src, {Uri sourceUrl}) {
    try {
      _parse(src, sourceUrl: sourceUrl);
    } on Exception {
      // ignore
    }
  }

  @override
  void accept(PubspecVisitor visitor) {
    if (author != null) {
      visitor.visitPackageAuthor(author);
    }
    if (authors != null) {
      visitor.visitPackageAuthors(authors);
    }
    if (description != null) {
      visitor.visitPackageDescription(description);
    }
    if (documentation != null) {
      visitor.visitPackageDocumentation(documentation);
    }
    if (homepage != null) {
      visitor.visitPackageHomepage(homepage);
    }
    if (name != null) {
      visitor.visitPackageName(name);
    }
    if (version != null) {
      visitor.visitPackageVersion(version);
    }
    if (dependencies != null) {
      visitor.visitPackageDependencies(dependencies);
      dependencies.forEach(visitor.visitPackageDependency);
    }
    if (devDependencies != null) {
      visitor.visitPackageDevDependencies(devDependencies);
      devDependencies.forEach(visitor.visitPackageDevDependency);
    }
    if (dependencyOverrides != null) {
      visitor.visitPackageDependencyOverrides(dependencyOverrides);
      dependencyOverrides.forEach(visitor.visitPackageDependencyOverride);
    }
  }

  @override
  String toString() {
    var sb = new _StringBuilder();
    sb.writelin(name);
    sb.writelin(version);
    sb.writelin(author);
    sb.writelin(authors);
    sb.writelin(description);
    sb.writelin(homepage);
    sb.writelin(dependencies);
    sb.writelin(devDependencies);
    sb.writelin(dependencyOverrides);
    return sb.toString();
  }

  _parse(String src, {Uri sourceUrl}) {
    var yaml = loadYamlNode(src, sourceUrl: sourceUrl);
    if (yaml is! YamlMap) {
      return;
    }
    YamlMap yamlMap = yaml;
    yamlMap.nodes.forEach((k, v) {
      if (k is! YamlScalar) {
        return;
      }
      YamlScalar key = k;
      switch (key.toString()) {
        case 'author':
          author = _processScalar(key, v);
          break;
        case 'authors':
          authors = _processList(key, v);
          break;
        case 'homepage':
          homepage = _processScalar(key, v);
          break;
        case 'name':
          name = _processScalar(key, v);
          break;
        case 'description':
          description = _processScalar(key, v);
          break;
        case 'documentation':
          documentation = _processScalar(key, v);
          break;
        case 'dependencies':
          dependencies = _processDependencies(key, v);
          break;
        case 'dev_dependencies':
          devDependencies = _processDependencies(key, v);
          break;
        case 'dependency_overrides':
          dependencyOverrides = _processDependencies(key, v);
          break;
        case 'version':
          version = _processScalar(key, v);
          break;
      }
    });
  }
}

class _StringBuilder {
  StringBuffer buffer = new StringBuffer();
  @override
  String toString() => buffer.toString();
  writelin(Object value) {
    if (value != null) {
      buffer.writeln(value);
    }
  }
}
