// Copyright 2013 The Flutter Authors
// 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:html/dom.dart';
import 'package:html/parser.dart' as html_parser;

import 'closed_caption_file.dart';

/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format.
/// See: https://en.wikipedia.org/wiki/WebVTT
class WebVTTCaptionFile extends ClosedCaptionFile {
  /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in
  /// the WebVTT file format.
  /// * See: https://en.wikipedia.org/wiki/WebVTT
  WebVTTCaptionFile(String fileContents) : _captions = _parseCaptionsFromWebVTTString(fileContents);

  @override
  List<Caption> get captions => _captions;

  final List<Caption> _captions;
}

List<Caption> _parseCaptionsFromWebVTTString(String file) {
  final captions = <Caption>[];

  // Ignore metadata
  final metadata = <String>{'HEADER', 'NOTE', 'REGION', 'WEBVTT'};

  var captionNumber = 1;
  for (final List<String> captionLines in _readWebVTTFile(file)) {
    // CaptionLines represent a complete caption.
    // E.g
    // [
    //  [00:00.000 --> 01:24.000 align:center]
    //  ['Introduction']
    // ]
    // If caption has just header or time, but no text, `captionLines.length` will be 1.
    if (captionLines.length < 2) {
      continue;
    }

    // If caption has header equal metadata, ignore.
    final String metadaType = captionLines[0].split(' ')[0];
    if (metadata.contains(metadaType)) {
      continue;
    }

    // Caption has header / identifier
    // See: https://www.w3.org/TR/webvtt1/#webvtt-cue-identifier for valid cue identifier
    final bool hasHeader = !captionLines[0].contains(_webVTTArrow);
    if (hasHeader) {
      final int? tryParseCaptionNumber = int.tryParse(captionLines[0]);
      if (tryParseCaptionNumber != null) {
        captionNumber = tryParseCaptionNumber;
      }
    }

    final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString(
      hasHeader ? captionLines[1] : captionLines[0],
    );

    if (captionRange == null) {
      continue;
    }

    final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n');

    // TODO(cyanglaz): Handle special syntax in VTT captions.
    // https://github.com/flutter/flutter/issues/90007.
    final String textWithoutFormat = _extractTextFromHtml(text);

    final newCaption = Caption(
      number: captionNumber,
      start: captionRange.start,
      end: captionRange.end,
      text: textWithoutFormat,
    );
    captions.add(newCaption);
    captionNumber++;
  }

  return captions;
}

class _CaptionRange {
  _CaptionRange(this.start, this.end);

  final Duration start;
  final Duration end;

  // Assumes format from an VTT file.
  // For example:
  // 00:09.000 --> 00:11.000
  static _CaptionRange? fromWebVTTString(String line) {
    final format = RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp);

    if (!format.hasMatch(line)) {
      return null;
    }

    final List<String> times = line.split(_webVTTArrow);

    final Duration? start = _parseWebVTTTimestamp(times[0]);
    final Duration? end = _parseWebVTTTimestamp(times[1]);

    if (start == null || end == null) {
      return null;
    }

    return _CaptionRange(start, end);
  }
}

String _extractTextFromHtml(String htmlString) {
  final Document document = html_parser.parse(htmlString);
  final Element? body = document.body;
  if (body == null) {
    return '';
  }
  final Element? bodyElement = html_parser.parse(body.text).documentElement;
  return bodyElement?.text ?? '';
}

// Parses a time stamp in an VTT file into a Duration.
//
// Returns `null` if `timestampString` is in an invalid format.
//
// For example:
//
// _parseWebVTTTimestamp('00:01:08.430')
// returns
// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430)
Duration? _parseWebVTTTimestamp(String timestampString) {
  if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) {
    return null;
  }

  final List<String> dotSections = timestampString.split('.');
  final List<String> timeComponents = dotSections[0].split(':');

  // Validating and parsing the `timestampString`, invalid format will result this method
  // to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid
  // WebVTT timestamp format.
  if (timeComponents.length > 3 || timeComponents.length < 2) {
    return null;
  }
  var hours = 0;
  if (timeComponents.length == 3) {
    final String hourString = timeComponents.removeAt(0);
    if (hourString.length < 2) {
      return null;
    }
    hours = int.parse(hourString);
  }
  final int minutes = int.parse(timeComponents.removeAt(0));
  if (minutes < 0 || minutes > 59) {
    return null;
  }
  final int seconds = int.parse(timeComponents.removeAt(0));
  if (seconds < 0 || seconds > 59) {
    return null;
  }

  final List<String> milisecondsStyles = dotSections[1].split(' ');

  // TODO(cyanglaz): Handle caption styles.
  // https://github.com/flutter/flutter/issues/90009.
  // ```dart
  // if (milisecondsStyles.length > 1) {
  //  List<String> styles = milisecondsStyles.sublist(1);
  // }
  // ```
  // For a better readable code style, style parsing should happen before
  // calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134.
  final int milliseconds = int.parse(milisecondsStyles[0]);

  return Duration(hours: hours, minutes: minutes, seconds: seconds, milliseconds: milliseconds);
}

// Reads on VTT file and splits it into Lists of strings where each list is one
// caption.
List<List<String>> _readWebVTTFile(String file) {
  final List<String> lines = LineSplitter.split(file).toList();

  final captionStrings = <List<String>>[];
  var currentCaption = <String>[];
  var lineIndex = 0;
  for (final line in lines) {
    final bool isLineBlank = line.trim().isEmpty;
    if (!isLineBlank) {
      currentCaption.add(line);
    }

    if (isLineBlank || lineIndex == lines.length - 1) {
      captionStrings.add(currentCaption);
      currentCaption = <String>[];
    }

    lineIndex += 1;
  }

  return captionStrings;
}

const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})';
const String _webVTTArrow = r' --> ';
