// Copyright (C) 2023 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter

import 'package:petitparser/petitparser.dart' hide Token;
import 'package:wyatt_i18n/src/core/utils/parser.dart' as wyatt;
import 'package:wyatt_i18n/src/domain/entities/tokens.dart';

/// {@template icu_parser}
/// A parser for ICU messages.
/// See https://unicode-org.github.io/icu/userguide/format_parse/messages/
/// for the syntax.
/// {@endtemplate}
class IcuParser extends wyatt.Parser<String, List<Token>?> {
  /// {@macro icu_parser}
  IcuParser() {
    // There is a cycle here, so we need the explicit
    // set to avoid infinite recursion.
    interiorText.set(contents.plus() | empty);
  }
  Parser get openCurly => char('{');

  Parser get closeCurly => char('}');

  Parser get quotedCurly => (string("'{'") | string("'}'")).map((x) => x[1]);

  Parser get icuEscapedText => quotedCurly | twoSingleQuotes;

  Parser get curly => openCurly | closeCurly;

  Parser get notAllowedInIcuText => curly | char('<');

  Parser get icuText => notAllowedInIcuText.neg();

  Parser get notAllowedInNormalText => char('{');

  Parser get normalText => notAllowedInNormalText.neg();

  Parser get messageText =>
      (icuEscapedText | icuText).plus().flatten().map(Literal.new);

  Parser get nonIcuMessageText => normalText.plus().flatten().map(Literal.new);

  Parser get twoSingleQuotes => string("''").map((x) => "'");

  Parser get number => digit().plus().flatten().trim().map(int.parse);

  Parser get id => (letter() & (word() | char('_')).star()).flatten().trim();

  Parser get comma => char(',').trim();

  /// Given a list of possible keywords, return a rule that accepts any of them.
  /// e.g., given ["male", "female", "other"], accept any of them.
  Parser asKeywords(List<String> list) =>
      list.map(string).cast<Parser>().reduce((a, b) => a | b).flatten().trim();

  Parser get pluralKeyword => asKeywords(
        ['=0', '=1', '=2', 'zero', 'one', 'two', 'few', 'many', 'other'],
      );

  Parser get genderKeyword => asKeywords(['female', 'male', 'other']);

  SettableParser<dynamic> interiorText = undefined();

  Parser get preface => (openCurly & id & comma).map((values) => values[1]);

  Parser get pluralLiteral => string('plural');

  Parser get pluralClause =>
      (pluralKeyword & openCurly & interiorText & closeCurly).trim().map(
            (result) => Option(
              result[0] as String,
              List<Token>.from(
                (result[2] is List ? result[2] as List : [result[2]])
                    .cast<Token>(),
              ),
            ),
          );

  Parser get plural =>
      preface & pluralLiteral & comma & pluralClause.plus() & closeCurly;

  Parser get intlPlural => plural.map(
        (result) => Plural(
          result[0] as String,
          List<Option>.from(result[3] as Iterable<dynamic>),
        ),
      );

  Parser get selectLiteral => string('select');

  Parser get genderClause =>
      (genderKeyword & openCurly & interiorText & closeCurly).trim().map(
            (result) => Option(
              result[0] as String,
              List<Token>.from(
                (result[2] is List ? result[2] as List : [result[2]])
                    .cast<Token>(),
              ),
            ),
          );

  Parser get gender =>
      preface & selectLiteral & comma & genderClause.plus() & closeCurly;

  Parser get intlGender => gender.map(
        (result) => Gender(
          result[0] as String,
          List<Option>.from(result[3] as Iterable<dynamic>),
        ),
      );

  Parser get selectClause =>
      (id & openCurly & interiorText & closeCurly).trim().map(
            (result) => Option(
              result[0] as String,
              List<Token>.from(
                (result[2] is List ? result[2] as List : [result[2]])
                    .cast<Token>(),
              ),
            ),
          );

  Parser get generalSelect =>
      preface & selectLiteral & comma & selectClause.plus() & closeCurly;

  Parser get intlSelect => generalSelect.map(
        (result) => Select(
          result[0] as String,
          List<Option>.from(result[3] as Iterable<dynamic>),
        ),
      );

  Parser get compound => (((parameter | nonIcuMessageText).plus() &
              pluralOrGenderOrSelect &
              (pluralOrGenderOrSelect | parameter | nonIcuMessageText).star()) |
          (pluralOrGenderOrSelect &
              (pluralOrGenderOrSelect | parameter | nonIcuMessageText).plus()))
      .map((result) => result.expand((x) => x is List ? x : [x]).toList());

  Parser get pluralOrGenderOrSelect => intlPlural | intlGender | intlSelect;

  Parser get contents => pluralOrGenderOrSelect | parameter | messageText;

  Parser get simpleText =>
      (nonIcuMessageText | parameter | openCurly).plus().map(
            (result) => result
                .map((item) => item is String ? Literal(item) : item)
                .toList(),
          );

  Parser get empty => epsilon().map((_) => Literal(''));

  Parser get parameter => (openCurly & id & closeCurly)
      .map((result) => Argument(result[1] as String));

  @override
  List<Token>? parse(String input) {
    final parsed = (compound | pluralOrGenderOrSelect | simpleText | empty)
        .map(
          (result) => List<Token>.from(result is List ? result : [result]),
        )
        .parse(input);

    return parsed.isSuccess ? parsed.value : null;
  }
}
