// Copyright (c) 2019, 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:convert';

import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/model/parameter.dart';

/// Render HTML in an extended vertical format using an `<ol>` tag.
class ParameterRendererHtmlList extends ParameterRendererHtml {
  const ParameterRendererHtmlList();

  @override
  bool get isBlock => true;

  @override
  String listItem(String item) => '<li>$item</li>\n';

  @override
  // TODO(jcollins-g): consider comma separated lists and more advanced CSS.
  String orderedList(
    String listItems, {
    bool multiLine = false,
    String openingBracket = '',
    String closingBracket = '',
  }) {
    var classes = [
      'parameter-list',
      if (!multiLine) 'single-line',
    ];
    return '$openingBracket'
        '<ol class="${classes.join(' ')}"> $listItems</ol>'
        '$closingBracket';
  }
}

/// Render HTML suitable for a single, wrapped line.
class ParameterRendererHtml extends ParameterRenderer {
  const ParameterRendererHtml();

  @override
  bool get isBlock => false;

  @override
  String listItem(String item) => item;

  @override
  String orderedList(
    String listItems, {
    bool multiLine = false,
    String openingBracket = '',
    String closingBracket = '',
  }) =>
      '$openingBracket$listItems$closingBracket';

  @override
  String annotation(String annotation) => '<span>$annotation</span>';

  @override
  String covariant(String covariant) => '<span>$covariant</span>';

  @override
  String defaultValue(String defaultValue) {
    var escaped =
        const HtmlEscape(HtmlEscapeMode.unknown).convert(defaultValue);
    return '<span class="default-value">$escaped</span>';
  }

  @override
  String parameter(String parameter, String id) =>
      '<span class="parameter" id="$id">$parameter</span>';

  @override
  String parameterName(String parameterName) =>
      '<span class="parameter-name">$parameterName</span>';

  @override
  String typeName(String typeName) =>
      '<span class="type-annotation">$typeName</span>';

  @override
  String required(String required) => '<span>$required</span>';
}

abstract class ParameterRenderer {
  const ParameterRenderer();

  bool get isBlock;

  String listItem(String item);
  String orderedList(
    String listItems, {
    bool multiLine = false,
    String openingBracket = '',
    String closingBracket = '',
  });
  String annotation(String annotation);
  String covariant(String covariant);
  String defaultValue(String defaultValue);
  String parameter(String parameter, String id);
  String parameterName(String parameterName);
  String typeName(String typeName);
  String required(String required);

  String renderLinkedParams(List<Parameter> parameters,
      {bool showMetadata = true}) {
    if (parameters.isEmpty) {
      return '';
    }

    var positionalParams = parameters
        .where((Parameter p) => p.isRequiredPositional)
        .toList(growable: false);
    var optionalPositionalParams = parameters
        .where((Parameter p) => p.isOptionalPositional)
        .toList(growable: false);
    var namedParams =
        parameters.where((Parameter p) => p.isNamed).toList(growable: false);
    var isMultiline =
        isBlock && (namedParams.isNotEmpty || parameters.length > 3);

    var buffer = StringBuffer();

    // If there are no required parameters, but optional positional or named
    // parameters, then we start with an opening bracket.
    var openingBracket = '';
    // If there are any optional positional or named parameters, then we end
    // with a closing bracket.
    var closingBracket = '';

    if (positionalParams.isEmpty) {
      if (optionalPositionalParams.isNotEmpty) {
        openingBracket = '[';
      } else if (namedParams.isNotEmpty) {
        openingBracket = '{';
      }
    } else {
      // Determine whether the line should end with an opening delimiter for
      // optional parameters.
      String trailingText;
      if (optionalPositionalParams.isNotEmpty) {
        trailingText = ', [';
      } else if (namedParams.isNotEmpty) {
        trailingText = ', {';
      } else if (isMultiline) {
        trailingText = ', ';
      } else {
        trailingText = '';
      }

      for (var p in positionalParams) {
        var suffix = identical(p, positionalParams.last) ? trailingText : ', ';
        buffer.write(
            _renderParameter(p, showMetadata: showMetadata, suffix: suffix));
      }
    }
    if (optionalPositionalParams.isNotEmpty) {
      closingBracket = ']';

      for (var p in optionalPositionalParams) {
        var suffix = isMultiline || !identical(p, optionalPositionalParams.last)
            ? ', '
            : '';
        buffer.write(
            _renderParameter(p, showMetadata: showMetadata, suffix: suffix));
      }
    }
    if (namedParams.isNotEmpty) {
      closingBracket = '}';

      for (var p in namedParams) {
        var suffix = isMultiline || !identical(p, namedParams.last) ? ', ' : '';
        buffer.write(
            _renderParameter(p, showMetadata: showMetadata, suffix: suffix));
      }
    }

    return orderedList(
      buffer.toString(),
      multiLine: isMultiline,
      openingBracket: openingBracket,
      closingBracket: closingBracket,
    );
  }

  String _renderParameter(
    Parameter param, {
    required bool showMetadata,
    required String suffix,
  }) {
    final buffer = StringBuffer();
    final modelType = param.modelType;

    if (showMetadata && param.hasAnnotations) {
      for (final a in param.annotations) {
        buffer.write('${annotation(a.linkedNameWithParameters)} ');
      }
    }
    if (param.isRequiredNamed) {
      buffer.write('${required('required')} ');
    }
    if (param.isCovariant) {
      buffer.write('${covariant('covariant')} ');
    }
    if (modelType is Callable) {
      final returnTypeName = modelType.isTypedef
          ? modelType.linkedName
          : modelType.returnType.linkedName;
      buffer.write(typeName(returnTypeName));
      buffer.write(' ${parameterName(param.documentedName ?? param.name)}');

      // Writes out the generic type parameters for a function type.
      // TODO(kallentu): Pull this type parameter generation into a helper for
      // other renderers that also do this same work.
      if (modelType is FunctionTypeElementType) {
        if (modelType.typeFormals.isNotEmpty) {
          if (!modelType.typeFormals.every((t) => t.name == 'dynamic')) {
            buffer
              ..write('&lt;<wbr><span class="type-parameter">')
              ..writeAll(modelType.typeFormals.map((t) => t.name),
                  '</span>, <span class="type-parameter">')
              ..write('</span>&gt;');
          }
        }
      }

      if (!modelType.isTypedef && modelType is DefinedElementType) {
        buffer.write('(');
        buffer.write(renderLinkedParams(
          (modelType as DefinedElementType).modelElement.parameters,
          showMetadata: showMetadata,
        ));
        buffer.write(')');
        buffer.write(modelType.nullabilitySuffix);
      }
      if (!modelType.isTypedef) {
        buffer.write('(');
        buffer.write(renderLinkedParams(
          modelType.parameters,
          showMetadata: showMetadata,
        ));
        buffer.write(')');
        buffer.write(modelType.nullabilitySuffix);
      }
    } else {
      final linkedTypeName = modelType.linkedName;
      if (linkedTypeName.isNotEmpty) {
        buffer.write(typeName(linkedTypeName));
        if (param.name.isNotEmpty) {
          buffer.write(' ');
        }
      }
      if (param.name.isNotEmpty) {
        buffer.write(parameterName(param.documentedName ?? param.name));
      }
    }

    if (param.hasDefaultValue) {
      buffer.write(' = ');
      buffer.write(defaultValue(param.defaultValue!));
    }

    buffer.write(suffix);
    var wrappedParameter = parameter(buffer.toString(), param.htmlId);
    return listItem(wrappedParameter);
  }
}
