// Copyright 2020 Ben Hills and the project contributors. 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:async';

import 'package:pinepods_mobile/bloc/podcast/audio_bloc.dart';
import 'package:pinepods_mobile/bloc/podcast/queue_bloc.dart';
import 'package:pinepods_mobile/entities/person.dart';
import 'package:pinepods_mobile/entities/transcript.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/state/queue_event_state.dart';
import 'package:pinepods_mobile/state/transcript_state_event.dart';
import 'package:pinepods_mobile/ui/widgets/platform_progress_indicator.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:url_launcher/url_launcher.dart';

/// This class handles the rendering of the podcast transcript (where available).
// ignore: must_be_immutable
class TranscriptView extends StatefulWidget {
  const TranscriptView({
    super.key,
  });

  @override
  State<TranscriptView> createState() => _TranscriptViewState();
}

class _TranscriptViewState extends State<TranscriptView> {
  final log = Logger('TranscriptView');
  final ItemScrollController _itemScrollController = ItemScrollController();
  final ScrollOffsetListener _scrollOffsetListener = ScrollOffsetListener.create(recordProgrammaticScrolls: false);
  final _transcriptSearchController = TextEditingController();
  late StreamSubscription<PositionState> _positionSubscription;
  int position = 0;
  bool autoScroll = true;
  bool autoScrollEnabled = true;
  bool forceTranscriptUpdate = false;
  bool first = true;
  bool scrolling = false;
  bool isHtmlTranscript = false;
  String speaker = '';
  RegExp exp = RegExp(r'(^)(\[?)(?<speaker>[A-Za-z0-9\s]+)(\]?)(\s?)(:)');

  @override
  void initState() {
    super.initState();
    final audioBloc = Provider.of<AudioBloc>(context, listen: false);

    Subtitle? subtitle;
    int index = 0;
    // If the user initiates scrolling, disable auto scroll.
    _scrollOffsetListener.changes.listen((event) {
      if (!scrolling) {
        setState(() {
          autoScroll = false;
        });
      }
    });

    // Listen to playback position updates and scroll to the correct items in the transcript
    // if we have auto scroll enabled.
    _positionSubscription = audioBloc.playPosition!.listen((event) {
      if (_itemScrollController.isAttached && !isHtmlTranscript) {
        var transcript = event.episode?.transcript;

        if (transcript != null && transcript.subtitles.isNotEmpty) {
          subtitle ??= transcript.subtitles[index];

          if (index == 0) {
            var match = exp.firstMatch(subtitle?.data ?? '');

            if (match != null) {
              setState(() {
                speaker = match.namedGroup('speaker') ?? '';
              });
            }
          }

          // Our we outside the range of our current transcript.
          if (event.position.inMilliseconds < subtitle!.start.inMilliseconds ||
              event.position.inMilliseconds > subtitle!.end!.inMilliseconds ||
              forceTranscriptUpdate) {
            forceTranscriptUpdate = false;
            // Will the next in the list do?
            if (transcript.subtitles.length > (index + 1) &&
                event.position.inMilliseconds >= transcript.subtitles[index + 1].start.inMilliseconds &&
                event.position.inMilliseconds < transcript.subtitles[index + 1].end!.inMilliseconds) {
              index++;
              subtitle = transcript.subtitles[index];

              if (subtitle != null && subtitle!.speaker.isNotEmpty) {
                speaker = subtitle!.speaker;
              } else {
                var match = exp.firstMatch(transcript.subtitles[index].data ?? '');

                if (match != null) {
                  speaker = match.namedGroup('speaker') ?? '';
                }
              }
            } else {
              try {
                subtitle = transcript.subtitles
                    .where((a) => (event.position.inMilliseconds >= a.start.inMilliseconds &&
                        event.position.inMilliseconds < a.end!.inMilliseconds))
                    .first;

                index = transcript.subtitles.indexOf(subtitle!);

                /// If we have had to jump more than one position within the transcript, we may
                /// need to back scan the conversation to find the current speaker.
                if (subtitle!.speaker.isNotEmpty) {
                  speaker = subtitle!.speaker;
                } else {
                  /// Scan backwards a maximum of 50 lines to see if we can find a speaker
                  var speakFound = false;
                  var count = 50;
                  var countIndex = index;

                  while (!speakFound && count-- > 0 && countIndex >= 0) {
                    var match = exp.firstMatch(transcript.subtitles[countIndex].data!);

                    countIndex--;

                    if (match != null) {
                      speaker = match.namedGroup('speaker') ?? '';

                      if (speaker.isNotEmpty) {
                        setState(() {
                          speakFound = true;
                        });
                      }
                    }
                  }
                }
              } catch (e) {
                // We don't have a transcript entry for this position.
              }
            }

            if (subtitle != null) {
              setState(() {
                position = subtitle!.start.inMilliseconds;
              });
            }

            if (autoScroll) {
              if (first) {
                _itemScrollController.jumpTo(index: index);
                first = false;
              } else {
                scrolling = true;
                _itemScrollController.scrollTo(index: index, duration: const Duration(milliseconds: 50)).then((value) {
                  scrolling = false;
                });
              }
            }
          }
        }
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _positionSubscription.cancel();
  }

  @override
  Widget build(BuildContext context) {
    final audioBloc = Provider.of<AudioBloc>(context, listen: false);
    final queueBloc = Provider.of<QueueBloc>(context, listen: false);

    return StreamBuilder<QueueState>(
        initialData: QueueEmptyState(),
        stream: queueBloc.queue,
        builder: (context, queueSnapshot) {
          return StreamBuilder<TranscriptState>(
              stream: audioBloc.nowPlayingTranscript,
              builder: (context, transcriptSnapshot) {
                if (transcriptSnapshot.hasData) {
                  if (transcriptSnapshot.data is TranscriptLoadingState) {
                    return const Align(
                      alignment: Alignment.center,
                      child: PlatformProgressIndicator(),
                    );
                  } else if (transcriptSnapshot.data is TranscriptUnavailableState ||
                      !transcriptSnapshot.data!.transcript!.transcriptAvailable) {
                    return Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Align(
                        alignment: Alignment.center,
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: [
                            Text(
                              'Transcript Error',
                              style: Theme.of(context).textTheme.titleLarge,
                              textAlign: TextAlign.center,
                            ),
                            const SizedBox(height: 16),
                            Text(
                              'Failed to load transcript. The episode has transcript support but there was an error retrieving or parsing the transcript data.',
                              style: Theme.of(context).textTheme.bodyMedium,
                              textAlign: TextAlign.center,
                            ),
                          ],
                        ),
                      ),
                    );
                  } else {
                    final items = transcriptSnapshot.data!.transcript?.subtitles ?? <Subtitle>[];
                    
                    // Detect if this is an HTML transcript (single item with HTMLFULL marker)
                    final isLikelyHtmlTranscript = items.length == 1 && 
                        items.first.data != null && 
                        items.first.data!.startsWith('{{HTMLFULL}}');
                    
                    // Update the state flag for HTML transcript detection
                    if (isLikelyHtmlTranscript != isHtmlTranscript) {
                      WidgetsBinding.instance.addPostFrameCallback((_) {
                        setState(() {
                          isHtmlTranscript = isLikelyHtmlTranscript;
                          if (isHtmlTranscript) {
                            autoScroll = false;
                            autoScrollEnabled = false;
                          }
                        });
                      });
                    }

                    return Column(
                      children: [
                        Padding(
                          padding: const EdgeInsets.only(top: 8.0, left: 16.0, right: 16.0),
                          child: TextField(
                            controller: _transcriptSearchController,
                            decoration: InputDecoration(
                              contentPadding: const EdgeInsets.all(0.0),
                              prefixIcon: const Icon(Icons.search),
                              suffixIcon: IconButton(
                                icon: const Icon(Icons.close),
                                onPressed: () {
                                  _transcriptSearchController.clear();
                                  audioBloc.filterTranscript(TranscriptClearEvent());
                                  setState(() {
                                    autoScrollEnabled = true;
                                  });
                                },
                              ),
                              isDense: true,
                              filled: true,
                              border: const OutlineInputBorder(
                                borderRadius: BorderRadius.all(Radius.circular(8.0)),
                                borderSide: BorderSide.none,
                                gapPadding: 0.0,
                              ),
                              hintText: L.of(context)!.search_transcript_label,
                            ),
                            onSubmitted: ((search) {
                              if (search.isNotEmpty) {
                                setState(() {
                                  autoScrollEnabled = false;
                                  autoScroll = false;
                                });
                                audioBloc.filterTranscript(TranscriptFilterEvent(search: search));
                              }
                            }),
                          ),
                        ),
                        if (!isHtmlTranscript)
                          Padding(
                            padding: const EdgeInsets.only(left: 8.0, right: 8.0),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.end,
                              crossAxisAlignment: CrossAxisAlignment.center,
                              children: [
                                Text(L.of(context)!.auto_scroll_transcript_label),
                                Switch(
                                  value: autoScroll,
                                  onChanged: autoScrollEnabled
                                      ? (bool enableAutoScroll) {
                                          setState(() {
                                            autoScroll = enableAutoScroll;

                                            if (enableAutoScroll) {
                                              forceTranscriptUpdate = true;
                                            }
                                          });
                                        }
                                      : null,
                                ),
                              ],
                            ),
                          ),
                        if (!isHtmlTranscript &&
                            queueSnapshot.hasData &&
                            queueSnapshot.data?.playing != null &&
                            queueSnapshot.data!.playing!.persons.isNotEmpty)
                          Container(
                            padding: const EdgeInsets.only(left: 16.0),
                            width: double.infinity,
                            height: 72.0,
                            child: ListView.builder(
                                scrollDirection: Axis.horizontal,
                                itemCount: queueSnapshot.data!.playing!.persons.length,
                                itemBuilder: (BuildContext context, int index) {
                                  var person = queueSnapshot.data!.playing!.persons[index];
                                  var selected = false;

                                  // Some speakers are - delimited so won't match
                                  speaker = speaker.replaceAll('-', ' ');

                                  if (speaker.isNotEmpty &&
                                      person.name.toLowerCase().startsWith(speaker.toLowerCase())) {
                                    selected = true;
                                  }

                                  return Padding(
                                    padding: const EdgeInsets.only(right: 8.0),
                                    child: Container(
                                      padding: const EdgeInsets.all(4.0),
                                      decoration: BoxDecoration(
                                          color: selected ? Colors.orange : Colors.transparent, shape: BoxShape.circle),
                                      child: CircleAvatar(
                                        radius: 28,
                                        backgroundImage: ExtendedImage.network(
                                          person.image!,
                                          cache: true,
                                        ).image,
                                        child: const Text(''),
                                      ),
                                    ),
                                  );
                                }),
                          ),
                        Expanded(
                          /// A simple way to ensure the builder is visible before attempting to use it.
                          child: LayoutBuilder(builder: (context, constraints) {
                            return constraints.minHeight > 60.0
                                ? Padding(
                                    padding: const EdgeInsets.only(top: 8.0),
                                    child: ScrollablePositionedList.builder(
                                        itemScrollController: _itemScrollController,
                                        scrollOffsetListener: _scrollOffsetListener,
                                        itemCount: items.length,
                                        itemBuilder: (BuildContext context, int index) {
                                          var i = items[index];
                                          return Wrap(
                                            children: [
                                              SubtitleWidget(
                                                subtitle: i,
                                                persons: queueSnapshot.data?.playing?.persons ?? <Person>[],
                                                highlight: i.start.inMilliseconds == position,
                                              ),
                                            ],
                                          );
                                        }),
                                  )
                                : Container();
                          }),
                        ),
                      ],
                    );
                  }
                } else {
                  return Container();
                }
              });
        });
  }
}

/// Each transcript is made up of one or more subtitles. Each [Subtitle] represents one
/// line of the transcript. This widget handles rendering the passed line.
class SubtitleWidget extends StatelessWidget {
  final Subtitle subtitle;
  final List<Person>? persons;
  final bool highlight;
  static const margin = Duration(milliseconds: 1000);

  const SubtitleWidget({
    super.key,
    required this.subtitle,
    this.persons,
    this.highlight = false,
  });

  @override
  Widget build(BuildContext context) {
    final audioBloc = Provider.of<AudioBloc>(context, listen: false);
    final data = subtitle.data ?? '';
    final isFullHtmlTranscript = data.startsWith('{{HTMLFULL}}');

    // For full HTML transcripts, render as a simple container without timing or clickability
    if (isFullHtmlTranscript) {
      return Container(
        width: double.infinity,
        padding: const EdgeInsets.all(16.0),
        child: _buildSubtitleContent(context),
      );
    }

    // For timed transcripts (JSON, SRT, chunked HTML), render with timing and clickability
    return GestureDetector(
      behavior: HitTestBehavior.translucent,
      onTap: () {
        final p = subtitle.start + margin;
        audioBloc.transitionPosition(p.inSeconds.toDouble());
      },
      child: Container(
        width: double.infinity,
        padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
        color: highlight ? Theme.of(context).cardTheme.color : Colors.transparent,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              subtitle.speaker.isEmpty
                  ? _formatDuration(subtitle.start)
                  : '${_formatDuration(subtitle.start)} - ${subtitle.speaker}',
              style: Theme.of(context).textTheme.titleSmall,
            ),
            _buildSubtitleContent(context),
            const Padding(padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 16.0))
          ],
        ),
      ),
    );
  }

  Widget _buildSubtitleContent(BuildContext context) {
    final data = subtitle.data ?? '';
    
    // Check if this is full HTML content (single document)
    if (data.startsWith('{{HTMLFULL}}')) {
      final htmlContent = data.substring(12); // Remove '{{HTMLFULL}}' marker
      
      return Html(
        data: htmlContent,
        style: {
          'body': Style(
            margin: Margins.zero,
            padding: HtmlPaddings.zero,
            fontSize: FontSize(Theme.of(context).textTheme.bodyMedium?.fontSize ?? 14),
            color: Theme.of(context).textTheme.bodyMedium?.color,
            fontFamily: Theme.of(context).textTheme.bodyMedium?.fontFamily,
            lineHeight: const LineHeight(1.5),
          ),
          'a': Style(
            color: Theme.of(context).primaryColor,
            textDecoration: TextDecoration.underline,
          ),
          'p': Style(
            margin: Margins.only(bottom: 12),
            padding: HtmlPaddings.zero,
          ),
          'h1, h2, h3, h4, h5, h6': Style(
            margin: Margins.only(top: 16, bottom: 8),
            fontWeight: FontWeight.bold,
          ),
          'strong, b': Style(
            fontWeight: FontWeight.bold,
          ),
          'em, i': Style(
            fontStyle: FontStyle.italic,
          ),
        },
        onLinkTap: (url, attributes, element) {
          if (url != null) {
            final uri = Uri.parse(url);
            launchUrl(uri);
          }
        },
      );
    } 
    // Check if this is chunked HTML content (legacy)
    else if (data.startsWith('{{HTML}}')) {
      final htmlContent = data.substring(8); // Remove '{{HTML}}' marker
      
      return Html(
        data: htmlContent,
        style: {
          'body': Style(
            margin: Margins.zero,
            padding: HtmlPaddings.zero,
            fontSize: FontSize(Theme.of(context).textTheme.titleMedium?.fontSize ?? 16),
            color: Theme.of(context).textTheme.titleMedium?.color,
            fontFamily: Theme.of(context).textTheme.titleMedium?.fontFamily,
          ),
          'a': Style(
            color: Theme.of(context).primaryColor,
            textDecoration: TextDecoration.underline,
          ),
          'p': Style(
            margin: Margins.zero,
            padding: HtmlPaddings.zero,
          ),
        },
        onLinkTap: (url, attributes, element) {
          if (url != null) {
            final uri = Uri.parse(url);
            launchUrl(uri);
          }
        },
      );
    } else {
      // Render as plain text for non-HTML content
      return Text(
        data,
        style: Theme.of(context).textTheme.titleMedium,
      );
    }
  }

  String _formatDuration(Duration duration) {
    final hh = (duration.inHours).toString().padLeft(2, '0');
    final mm = (duration.inMinutes % 60).toString().padLeft(2, '0');
    final ss = (duration.inSeconds % 60).toString().padLeft(2, '0');

    return '$hh:$mm:$ss';
  }
}
