package org.vaadin.firitin.components.messagelist;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.messages.MessageList;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.Style;
import com.vaadin.flow.server.Command;
import in.virit.color.HexColor;
import org.vaadin.firitin.components.RichText;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * A wrapper for vaadin-message web component, that supports Markdown
 * formatting and appending content dynamically to the element. Typical
 * usecase: LLM chatbots slowly giving you the answer.
 */
@Tag("vaadin-message")
@Uses(MessageList.class)
public class MarkdownMessage extends Component implements HasStyle, HasSize {

    private static final String PLACEHOLDER = "...";

    public interface Color {

        // "Stolen" from https://github.com/vaadin/web-components/blob/1875686236814dcc065a0e067c87adb80153ce60/packages/vaadin-lumo-styles/user-colors.js#L12
        public static in.virit.color.Color[] AVATAR_PRESETS = new in.virit.color.Color[] {
                HexColor.of("#df0b92"),
                HexColor.of("#650acc"),
                HexColor.of("#097faa"),
                HexColor.of("#ad6200"),
                HexColor.of("#bf16f3"),
                HexColor.of("#084391"),
                HexColor.of("#078836")
        };
    };

    private UI ui;

    private String previousHtml;

    private boolean autoScroll = true;

    private Element content = new Element("div");
    private Element scrollHelper = new Element("div");
    private MarkdownStrategy markdownStrategy;

    /**
     * Constructs a new MarkdownMessages with all the bells and whistles,
     * without initial content. Add that later with {@link #appendMarkdownAsync(String)}.
     *
     * @param name the name of the user
     * @param timestamp time of the message
     * @param color the color used for user avatar
     */
    public MarkdownMessage(String name, LocalDateTime timestamp, in.virit.color.Color color) {
        getElement().setProperty("userName", name);
        getElement().setProperty("time", timestamp.format(DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm")));
        getElement().appendChild(content, scrollHelper);
        if(color != null) {
            setAvatarColor(color);
        }
        content.getStyle().setWhiteSpace(Style.WhiteSpace.NORMAL);
    }

    /**
     * Constructs a new MarkdownMessages, without initial content.
     * Add that later with {@link #appendMarkdownAsync(String)}.
     *
     * @param name the name of the user
     */
    public MarkdownMessage(String name) {
        this(name, LocalDateTime.now(), Color.AVATAR_PRESETS[0]);
        setMarkdown(null);
    }

    /**
     * Constructs a new MarkdownMessages, without initial content.
     * Add that later with {@link #appendMarkdownAsync(String)}.
     *
     * @param name the name of the user
     * @param timestamp the timestamp of the message
     */
    public MarkdownMessage(String name, LocalDateTime timestamp) {
        this(name, timestamp,Color.AVATAR_PRESETS[0]);
        setMarkdown(null);
    }

    /**
     * Constructs a new MarkdownMessages, without initial content.
     * Add that later with {@link #appendMarkdownAsync(String)}.
     *
     * @param name the name of the user
     * @param avatarColor the avatar color
     */
    public MarkdownMessage(String name, in.virit.color.Color avatarColor) {
        this(name, LocalDateTime.now(), avatarColor);
        setMarkdown(null);
    }

    /**
     * Constructs a new MarkdownMessages, with initial content.
     * You can add more text later with {@link #appendMarkdownAsync(String)}.
     *
     *
     * @param markdown the initial content as markdown formatter text
     * @param name the name of the user
     * @param avatarColor the color of the avatar
     */
    public MarkdownMessage(String markdown, String name, in.virit.color.Color avatarColor) {
        this(name, LocalDateTime.now(), avatarColor);
        setMarkdown(markdown);
    }

    /**
     * Constructs a new MarkdownMessages, with initial content.
     * You can add more text later with {@link #appendMarkdownAsync(String)}.
     *
     *
     * @param markdown the initial content as markdown formatter text
     * @param name the name of the user
     */
    public MarkdownMessage(String markdown, String name) {
        this(name, LocalDateTime.now(), Color.AVATAR_PRESETS[name.hashCode()%Color.AVATAR_PRESETS.length]);
        setMarkdown(markdown);
    }

    /**
     * Constructs a new MarkdownMessages, with initial content.
     * You can add more text later with {@link #appendMarkdownAsync(String)}.
     *
     * @param markdown the initial content as markdown formatted text
     * @param name the name of the user
     * @param timestamp the timestamp of the message
     */
    public MarkdownMessage(String markdown, String name, LocalDateTime timestamp) {
        this(name, timestamp, null);
        setMarkdown(markdown);
    }

    public void setAvatarColor(in.virit.color.Color color) {
        getElement().getStyle().set("--vaadin-avatar-user-color", color.toString());
        // remove the once set by constructor && ensure the flag making it use

        getElement().executeJs("\n" +
                "$0.querySelector('vaadin-avatar').style.setProperty('--vaadin-avatar-user-color', null);$0.querySelector('vaadin-avatar').setAttribute('has-color-index', true);");

    }

    public void setUserColorIndex(int index) {
        getElement().setProperty("userColorIndex", index);
    }

    protected MarkdownStrategy getMarkdownStrategy() {
        if(markdownStrategy == null) {
            markdownStrategy = new MarkdownItStrategy();
        }
        return markdownStrategy;
    }

    protected void setMarkdown(String markdown, boolean uiAccess) {
        getMarkdownStrategy().setMarkdown(markdown, uiAccess);
    }

    public void setMarkdown(String markdown) {
        setMarkdown(markdown,false);
    }

    private void appendHtml(String html) {
        getElement().executeJs("""
                if(this.curHtml) {
                    this.curHtml = this.curHtml + $0;
                } else {
                    this.curHtml = $0;
                }
                $1.innerHTML = this.curHtml;
                """, html, content);
    }
    private void appendHtml(String html, int replaceFrom) {
        getElement().executeJs("""
                this.curHtml = this.curHtml ? this.curHtml.substring(0, $2) + $0 : $0; 
                $1.innerHTML = this.curHtml;
                """, html, content, replaceFrom);
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        super.onAttach(attachEvent);
        this.ui = attachEvent.getUI();
    }

    public UI getUi() {
        if(ui == null) {
            // fallback, but not 100% thread safe, thanks to all involved :-)
            ui = ui.getUI().orElseGet(() -> UI.getCurrent());
        }
        return ui;
    }

    /**
     * Directly adds markdown formatted text to message part.
     * Note, that this method should not be called from any other but
     * Vaadin UI thread. Consider using the {@link #appendMarkdownAsync(String)}
     * version in case the new text is coming from a background thread.
     *
     * @param markdownSnippet the new markdown formatted text snippet
     */
    public void appendMarkdown(String markdownSnippet) {
        appendMarkdown(markdownSnippet, false);
    }

    /**
     * Adds markdown formatted text to message part. This method takes care
     * of synchronization with {@link UI#access(Command)}, so it is safe to call
     * this directly from a background thread.
     * 
     * @param markdownSnippet the new markdown formatted text snippet
     */
    public void appendMarkdownAsync(String markdownSnippet) {
        appendMarkdown(markdownSnippet, true);
    }

    protected void appendMarkdown(String markdownSnippet, boolean uiAccess) {
        getMarkdownStrategy().appendMarkdown(markdownSnippet, uiAccess);
    }

    public boolean isAutoScroll() {
        return autoScroll;
    }

    public void setAutoScroll(boolean autoScroll) {
        this.autoScroll = autoScroll;
    }

    protected void doAutoScroll() {
        if(autoScroll) {
            scrollHelper.executeJs("""
                if(this.scrollIntoViewIfNeeded) {
                    this.scrollIntoViewIfNeeded();
                } else {
                    // FF
                    this.scrollIntoView();
                }
            """);
        }
    }

    interface MarkdownStrategy {
        void appendMarkdown(String markdown, boolean uiAccess);
        void setMarkdown(String markdown, boolean uiAccess);
    }

    /**
     * The new default strategy in 2.13.0. This does not keep value on the
     * server side state at all, but pushes the content for browser to render.
     * Less server utilization, but less flexible and a bit less secure.
     */
    class MarkdownItStrategy implements MarkdownStrategy {

        @Override
        public void appendMarkdown(String markdown, boolean uiAccess) {
            Command c = () -> {
                RichText.MarkdownItStrategy.ensureMarkdownIt();
                content.executeJs("""
                    const md = window.markdownit();\s
                    const input = $0;
                    if(this.markdown) {
                        this.markdown = this.markdown + input;
                    } else {
                        this.markdown = input;
                    }
                    const html = md.render(this.markdown);
                    this.innerHTML = html;
                """, markdown);
            };
            if (uiAccess) {
                getUi().access(c);
            } else {
                c.execute();
            }
        }

        @Override
        public void setMarkdown(String markdown, boolean uiAccess) {
            Command c = () -> {
                RichText.MarkdownItStrategy.ensureMarkdownIt();
                content.executeJs("""
                    const md = window.markdownit();\s
                    const input = $0;
                    this.markdown = input;
                    const html = md.render(input);
                    this.innerHTML = html;
                """, (markdown == null) ? "" : markdown);
            };
            if (uiAccess) {
                getUi().access(c);
            } else {
                c.execute();
            }


        }

    }


}
