// Copyright (c) 2022, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

import $ from 'jquery';
import {pluck} from 'underscore';
import {sortedList, HistoryEntry, EditorSource} from '../history.js';
import {editor} from 'monaco-editor';

import IStandaloneDiffEditor = editor.IStandaloneDiffEditor;
import ITextModel = editor.ITextModel;
import {unwrap} from '../assert.js';

export class HistoryDiffState {
    public model: ITextModel;
    private result: EditorSource[];

    constructor(model: ITextModel) {
        this.model = model;
        this.result = [];
    }

    update(result: HistoryEntry) {
        this.result = result.sources;
        this.refresh();

        return true;
    }

    private refresh() {
        const output = this.result;
        const content = output.map(val => `/****** ${val.lang} ******/\n${val.source}`).join('\n');

        this.model.setValue(content);
    }
}

type Entry = {dt: number; name: string; load: () => void};

export class HistoryWidget {
    private modal: JQuery | null;
    private diffEditor: IStandaloneDiffEditor | null;
    private lhs: HistoryDiffState | null;
    private rhs: HistoryDiffState | null;
    private currentList: HistoryEntry[];
    private onLoadCallback: (data: HistoryEntry) => void;

    constructor() {
        this.modal = null;
        this.diffEditor = null;
        this.lhs = null;
        this.rhs = null;
        this.currentList = [];
        this.onLoadCallback = () => {};
    }

    private initializeIfNeeded() {
        if (this.modal === null) {
            this.modal = $('#history');

            const placeholder = this.modal.find('.monaco-placeholder');
            this.diffEditor = editor.createDiffEditor(placeholder[0], {
                fontFamily: 'Consolas, "Liberation Mono", Courier, monospace',
                scrollBeyondLastLine: true,
                readOnly: true,
                // language: 'c++',
                minimap: {
                    enabled: true,
                },
            });

            this.lhs = new HistoryDiffState(editor.createModel('', 'c++'));
            this.rhs = new HistoryDiffState(editor.createModel('', 'c++'));
            this.diffEditor.setModel({original: this.lhs.model, modified: this.rhs.model});

            this.modal.find('.inline-diff-checkbox').on('click', event => {
                const inline = $(event.target).prop('checked');
                this.diffEditor?.updateOptions({
                    renderSideBySide: !inline,
                });
                this.resizeLayout();
            });
        }
    }

    private static getLanguagesFromHistoryEntry(entry: HistoryEntry) {
        return pluck(entry.sources, 'lang');
    }

    private populateFromLocalStorage() {
        this.currentList = sortedList();
        this.populate(
            unwrap(this.modal).find('.historiccode'),
            this.currentList.map((data): Entry => {
                const dt = new Date(data.dt).toString();
                const languages = HistoryWidget.getLanguagesFromHistoryEntry(data).join(', ');
                return {
                    dt: data.dt,
                    name: `${dt.replace(/\s\(.*\)/, '')} (${languages})`,
                    load: () => {
                        this.onLoad(data);
                        this.modal?.modal('hide');
                    },
                };
            }),
        );
    }

    private hideRadiosAndSetDiff() {
        const root = unwrap(this.modal).find('.historiccode');
        const items = root.find('li:not(.template)');

        let foundbase = false;
        let foundcomp = false;

        for (const elem of items) {
            const li = $(elem);
            const dt = li.data('dt');

            const base = li.find('.base');
            const comp = li.find('.comp');

            let baseShouldBeVisible = true;
            let compShouldBeVisible = true;

            if (comp.prop('checked')) {
                foundcomp = true;
                baseShouldBeVisible = false;

                const itemRight = this.currentList.find(item => item.dt === dt);
                if (itemRight) {
                    unwrap(this.rhs).update(itemRight);
                }
            } else if (base.prop('checked')) {
                foundbase = true;

                const itemLeft = this.currentList.find(item => item.dt === dt);
                if (itemLeft) {
                    unwrap(this.lhs).update(itemLeft);
                }
            }

            if (foundbase && foundcomp) {
                compShouldBeVisible = false;
            } else if (!foundbase && !foundcomp) {
                baseShouldBeVisible = false;
            }

            if (compShouldBeVisible) {
                comp.css('visibility', '');
            } else {
                comp.css('visibility', 'hidden');
            }

            if (baseShouldBeVisible) {
                base.css('visibility', '');
            } else {
                base.css('visibility', 'hidden');
            }
        }
    }

    private populate(root: JQuery, list: Entry[]) {
        root.find('li:not(.template)').remove();
        const template = root.find('.template');

        let baseMarked = false;
        let compMarked = false;

        for (const elem of list) {
            const li = template.clone().removeClass('template').appendTo(root);

            li.data('dt', elem.dt);

            const base = li.find('.base');
            const comp = li.find('.comp');

            if (!compMarked) {
                comp.prop('checked', 'checked');
                compMarked = true;
            } else if (!baseMarked) {
                base.prop('checked', 'checked');
                baseMarked = true;
            }

            base.on('click', () => this.hideRadiosAndSetDiff());
            comp.on('click', () => this.hideRadiosAndSetDiff());

            li.find('a').text(elem.name).on('click', elem.load);
        }

        this.hideRadiosAndSetDiff();
    }

    private resizeLayout() {
        const tabcontent = unwrap(this.modal).find('div.tab-content');
        this.diffEditor?.layout({
            width: unwrap(tabcontent.width()),
            height: unwrap(tabcontent.height()) - 20,
        });
    }

    private onLoad(data: HistoryEntry) {
        this.onLoadCallback(data);
    }

    run(onLoad: (data: HistoryEntry) => void) {
        this.initializeIfNeeded();
        this.populateFromLocalStorage();
        this.onLoadCallback = onLoad;

        // It can't tell that we initialize modal on initializeIfNeeded, so it sticks to the possibility of it being null
        unwrap(this.modal).on('shown.bs.modal', () => this.resizeLayout());
        unwrap(this.modal).modal();
    }
}
