import { Component, Prop, Vue } from 'vue-property-decorator';
// @ts-ignore // FIXME Нет поддержки *.ts, добавить, когда появится
import Modeler from 'bpmn-js/lib/Modeler';
// @ts-ignore // FIXME Нет поддержки *.ts, добавить, когда появится
import propertiesPanelModule from 'bpmn-js-properties-panel';
// @ts-ignore // FIXME Нет поддержки *.ts, добавить, когда появится
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
// @ts-ignore // FIXME Нет поддержки *.ts, добавить, когда появится
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import BaseBpmnCloudLoader from '@components/common/base-bpmn-cloud-loader/index.vue';
import { saveFileViaLink } from '@utils/files';
import { WorkflowServiceApi } from '@openapi/wf';
import { uploadBPMN } from '@services/wf';
import { notifyError } from '@utils/notify';

const wfApi = new WorkflowServiceApi();

@Component({
  components: {
    BaseBpmnCloudLoader,
  },
})
export default class UserTasksBpmnEditorComponent extends Vue {
  @Prop({
    default: '',
  })
  protected schema!: string;

  private bpmnEditor: Modeler | null = null;
  private menu = false;
  private cloud = {
    loading: false,
    dialog: false,
  };

  private splitter = 80;
  // todo check usage replace with i18n locales
  private translations = {
    'Activate the global connect tool':
      'Включить иструмент «Глобальное соединение»',
    'Append {type}': 'Добавить {type}',
    'Append Task': 'Добавить задачу',
    'Add Lane above': 'Добавить дорожку сверху',
    'Divide into two Lanes': 'Разделить на две дорожки',
    'Divide into three Lanes': 'Разделить на три дорожки',
    'Add Lane below': 'Добавить дорожку снизу',
    'Append compensation activity': 'Добавить компенсирующее действие',
    'Change type': 'Изменить тип',
    'Connect using Association': 'Соединить с использованием ассоциации',
    'Connect using Sequence/MessageFlow or Association':
      'Соединить с использованием потока управления/сообщений или ассоциации',
    'Connect using DataInputAssociation':
      'Соединить с использованием ассоциации ввода данных',
    Remove: 'Удалить',
    'Activate the hand tool': 'Включить инструмент «Рука»',
    'Activate the lasso tool': 'Включить инструмент «Лассо»',
    'Activate the create/remove space tool':
      'Включить инструмент «Управление пространством»',
    'Create expanded SubProcess': 'Создать развернутый подпроцесс',
    'Create Intermediate/Boundary Event':
      'Создать промежуточное/граничное событие',
    'Append Intermediate/Boundary Event':
      'Добавить промежуточное/граничное событие',
    'Create IntermediateThrowEvent/BoundaryEvent':
      'Создать промежуточное событие-инициатор/граничное событие',
    'Create Pool/Participant': 'Создать пул/участника',
    'Parallel Multi Instance': 'Параллельное выполнение',
    'Sequential Multi Instance': 'Последовательное выполнение',
    Loop: 'Цикл',
    'Ad-hoc': 'Ad-hoc',
    'Create {type}': 'Создать {type}',
    'Create Task': 'Создать задачу',
    'Create Group': 'Создать группу',
    Task: 'Задача',
    'Send Task': 'Задача отправки сообщения',
    'Receive Task': 'Задача получения сообщения',
    'User Task': 'Задача, выполняемая пользователем',
    'Manual Task': 'Задача, выполняемая вручную',
    'Business Rule Task': 'Задача выполнения бизнес-правила',
    'Service Task': 'Задача-сервис',
    'Script Task': 'Задача-сценарий',
    'Call Activity': 'Действие «Вызов»',
    'Sub Process (collapsed)': 'Свернутый подпроцесс',
    'Sub Process (expanded)': 'Развернутый подпроцесс',
    'Start Event': 'Начальное событие',
    'Intermediate/Boundary Event': 'Промежуточное/граничное событие',
    'Intermediate Throw Event': 'Промежуточное событие-инициатор',
    'End Event': 'Конечное событие',
    'Message Start Event': 'Начальное событие-сообщение',
    'Timer Start Event': 'Начальное событие-таймер',
    'Conditional Start Event': 'Начальное событие-условие',
    'Signal Start Event': 'Начальное событие-сигнал',
    'Error Start Event': 'Начальное событие-ошибка',
    'Escalation Start Event': 'Начальное событие-эскалация',
    'Compensation Start Event': 'Начальное событие-компенсация',
    'Message Start Event (non-interrupting)':
      'Начальное событие-сообщение (без прерываний)',
    'Timer Start Event (non-interrupting)':
      'Начальное событие-таймер (без прерываний)',
    'Conditional Start Event (non-interrupting)':
      'Начальное событие-условие (без прерываний)',
    'Signal Start Event (non-interrupting)':
      'Начальное событие-сигнал (без прерываний)',
    'Escalation Start Event (non-interrupting)':
      'Начальное событие-эскалация (без прерываний)',
    'Message Intermediate Catch Event':
      'Обработчик промежуточного события-сообщения',
    'Message Intermediate Throw Event':
      'Инициатор промежуточного события-сообщения',
    'Timer Intermediate Catch Event':
      'Обработчик промежуточного события-таймера',
    'Escalation Intermediate Throw Event':
      'Инициатор промежуточного события-эскалации',
    'Conditional Intermediate Catch Event':
      'Обработчик промежуточного события-условия',
    'Link Intermediate Catch Event': 'Обработчик промежуточного события-ссылки',
    'Link Intermediate Throw Event': 'Инициатор промежуточного события-ссылки',
    'Compensation Intermediate Throw Event':
      'Инициатор промежуточного события-компенсации',
    'Signal Intermediate Catch Event':
      'Обработчик промежуточного события-сигнала',
    'Signal Intermediate Throw Event':
      'Инициатор промежуточного события-сигнала',
    'Message End Event': 'Конечное событие-сообщение',
    'Escalation End Event': 'Конечное событие-эскалация',
    'Error End Event': 'Конечное событие-ошибка',
    'Cancel End Event': 'Конечное событие-отмена',
    'Compensation End Event': 'Конечное событие-компенсация',
    'Signal End Event': 'Конечное событие-сигнал',
    'Terminate End Event': 'Конечное событие-остановка',
    'Message Boundary Event': 'Граничное событие-сообщение',
    'Message Boundary Event (non-interrupting)':
      'Граничное событие-сообщение (без прерываний)',
    'Timer Boundary Event': 'Граничное событие-таймер',
    'Timer Boundary Event (non-interrupting)':
      'Граничное событие-таймер (без прерываний)',
    'Escalation Boundary Event': 'Граничное событие-эскалация',
    'Escalation Boundary Event (non-interrupting)':
      'Граничное событие-эскалация (без прерываний)',
    'Conditional Boundary Event': 'Граничное событие-условие',
    'Conditional Boundary Event (non-interrupting)':
      'Граничное событие-условие (без прерываний)',
    'Error Boundary Event': 'Граничное событие-ошибка',
    'Cancel Boundary Event': 'Граничное событие-отмена',
    'Signal Boundary Event': 'Граничное событие-сигнал',
    'Signal Boundary Event (non-interrupting)':
      'Граничное событие-сигнал (без прерываний)',
    'Compensation Boundary Event': 'Граничное событие-компенсация',
    'Exclusive Gateway': 'Шлюз «исключающее или»',
    'Parallel Gateway': 'Шлюз «и»',
    'Inclusive Gateway': 'Шлюз «или»',
    'Complex Gateway': 'Комплексный шлюз',
    'Event based Gateway': 'Шлюз по событиям',
    Transaction: 'Транзакция',
    'Sub Process': 'Подпроцесс',
    'Event Sub Process': 'Подпроцесс по событию',
    'Collapsed Pool': 'Свёрнутый пул',
    'Expanded Pool': 'Развёрнутый пул',
    'Data Object Reference': 'Объект данных',
    'Data Store Reference': 'Хранилище данных',
    Group: 'Группа',
    'Text Annotation': 'Комментарий',

    // Errors
    'no parent for {element} in {parent}':
      'Нет родителя для {element} в {parent}',
    'no shape type specified': 'Тип фигуры не указан',
    'flow elements must be children of pools/participants':
      'Элементы потока должны быть дочерними элементами пула/участников',
    'out of bounds release': 'Выход за границы',
    'more than {count} child lanes': 'Более чем {count} дочерних дорожек',
    'element required': 'Требуется элемент',
    'diagram not part of bpmn:Definitions':
      'Диаграмма не является частью bpmn:Definitions',
    'no diagram to display': 'Нет диаграммы для отображения',
    'no process or collaboration to display':
      'Нет процесса или взаимодействия для отображения',
    'element {element} referenced by {referenced}#{property} not yet drawn':
      'Элемент {element}, на который ссылается {referenced}#{property}, еще не отрисован',
    'already rendered {element}': '{element} уже отрисован',
    'failed to import {element}': 'Не удалось импортировать {element}',

    // Panel
    General: 'Общее',
    Id: 'Идентификатор',
    Name: 'Наименование',
    'This maps to the process definition key.': 'Соответствует ключу процесса',
    'Version Tag': 'Тег версии',
    Executable: 'Выполнимо',
    'External Task Configuration': 'Конфигурация внешней задачи',
    'Task Priority': 'Приоритетность задачи',
    'Job Configuration': 'Конфигурация задания',
    'Job Priority': 'Приоритетность задания',
    'Candidate Starter Configuration': 'Конфигурация базовых групп ',
    'Candidate Starter Groups': 'Базовые группы',
    'Specify more than one group as a comma separated list.':
      'Укажите несколько групп в виде списка, разделенного запятыми',
    'Candidate Starter Users': 'Базовые пользователи',
    'Specify more than one user as a comma separated list.':
      'Укажите более одного пользователя в виде списка, разделенного запятыми',
    'History Configuration': 'Конфигурация ведения истории',
    'History Time To Live': 'Время жизни записей истории',
    'Tasklist Configuration': 'Конфигурация списка задач',
    Startable: 'Запускаемый',
    Documentation: 'Документация',
    'Element Documentation': 'Документация по элементам',
    Listeners: 'Слушатели',
    'Execution Listeners': 'Слушатели выполнения',
    'Execution Listener': 'Слушатель выполнения',
    'Event Type': 'Тип события',
    'Must provide a value': 'Необходимо указать значение',
    start: 'начало',
    end: 'завершение',
    Listener: 'Слушатель',
    'Listener Type': 'Тип слушателя',
    'Java Class': 'Java-класс',
    Expression: 'Выражения',
    'Delegate Expression': 'Выражение-делегат',
    Script: 'Сценарий',
    'Field Injection': 'Ввод поля',
    Fields: 'Поля',
    'Script Type': 'Тип сценария',
    'Inline Script': 'Встроенный сценарий',
    'External Resource': 'Внешний ресурс',
    Resource: 'Ресурсы',
    Extensions: 'Расширения',
    Properties: 'Характеристики',
    'Add Property': 'Добавить характеристику',
    Value: 'Значение',
    'Input/Output': 'Ввод/Вывод',
    Parameters: 'Параметры',
    'Input Parameters': 'Входящие параметры',
    'Output Parameters': 'Исходящие параметры',
    'Input Parameter': 'Входящий параметр',
    Type: 'Тип',
    'Fields Injectionns': 'Инъекции полей',
    'Fields Injections': 'Инъекции полей',
    Forms: 'Формы',
    'Form Key': 'Ключ формы',
    'Form Fields': 'Поля формы',
    'Form Field': 'Поле формы',
    ID: 'Идентификатор',
    Label: 'Лейбл',
    'Default Value': 'Значение по умолчанию',
    Validation: 'Валидация',
    'Add Constraint': 'Добавить ограничение',
    Config: 'Конфигурация',
    Details: 'Детали',
    Message: 'Сообщение',
    'Asynchronous Continuations': 'Асинхронное продолжение',
    'Asynchronous Before': 'Асинхронный "до"',
    'Asynchronous After': 'Асинхронный "после"',
    Exclusive: 'Эксклюзивный',
    'Retry Time Cycle': 'Периодичность выполнения',
    Implementation: 'Реализация',
    Assignee: 'Исполнитель',
    'Candidate Users': 'Возможные пользователи',
    'Candidate Groups': 'Возможные группы',
    'Due Date': 'Срок исполнения',
    // eslint-disable-next-line no-template-curly-in-string
    'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
      // eslint-disable-next-line no-template-curly-in-string
      'Регулярное выражение (например, ${someDate} или дата в формате ISO (например, 2015-06-26T09:54:00)',
    'Follow Up Date': 'Контрольная дата',
    Priority: 'Приоритет',
    'Must configure Connector': 'Необходимо настроить коннектор',
    Connector: 'Коннектор',
    'Connector Id': 'Идентификатор коннектора',
    Text: 'Текст',
    List: 'Список',
    Map: 'Соответствие',
    'In Mapping': 'Входящий маппинг',
    'Out Mapping': 'Исходящий маппинг',
    Topic: 'Категория',
    External: 'Внешний',
    'This maps to the task definition key.': 'Соответствует ключу задачи',
    // eslint-disable-next-line no-template-curly-in-string
    'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
      // eslint-disable-next-line no-template-curly-in-string
      'Контрольная дата - это регулярное выражение (например, ${someDate} или дата в формате ISO (например, 2015-06-26T09:54:00)',
    'Called Element': 'Вызываемый объект',
    'CallActivity Type': 'Тип вызова',
    'Business Key': 'Бизнес-ключ',
    'Business Key Expression': 'Выражение для бизнес-ключа',
    'Delegate Variable Mapping': 'Маппинг переменных делегата',
    Variables: 'Переменные',
    'Configure Connector': 'Настройка конвектора',
    Binding: 'Привязка',
    'Tenant Id': 'Идентификатор тенанта',
    'Field Injections': 'Добавление полей',
  };

  private init(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        const customTranslateModule: any = {
          translate: ['value', this.customTranslate],
        };

        this.bpmnEditor = new Modeler({
          additionalModules: [
            propertiesPanelModule,
            propertiesProviderModule,
            customTranslateModule,
          ],
          container: this.$refs.container,
          moddleExtensions: {
            camunda: camundaModdleDescriptor,
          },
          propertiesPanel: {
            parent: this.$refs.properties,
          },
        });

        return resolve();
      } catch (e) {
        return reject(e);
      }
    });
  }

  private bpmnSchemaUploadHandler(val: UploaderValue) {
    const xml: string = val.value.toString();
    this.clear().then(() => {
      this.importXML(xml);
    });
  }

  private customTranslate(template: any, replacements: any) {
    replacements = replacements || {};
    // @ts-ignore
    template = this.translations[template] || template;

    return template.replace(/{([^}]+)}/g, function (_: any, key: any) {
      return replacements[key] || '{' + key + '}';
    });
  }

  protected selectHandler(schema?: string): void {
    this.cloud.dialog = false;

    this.cloud.loading = true;

    let code: string = get(this.cloud, 'selected[0].key', '');

    if (isEmpty(code) && !isEmpty(schema)) {
      code = schema!;
    }

    wfApi
      .getSchema(code)
      .then(({ data: response }) => {
        // todo OPENAPI response void
        // @ts-ignore
        const xml: string = (response || '').toString();

        this.clear().then(() => {
          this.importXML(xml);
        });
      })
      .finally(() => {
        this.cloud.loading = false;
      });
  }

  importHandler(type: string): void {
    switch (type) {
      case 'cloud':
        this.cloud.dialog = true;
        break;
      case 'device':
        (this.$refs.import as HTMLElement).click();
        break;
    }
  }

  exportHandler(type: string): void {
    switch (type) {
      case 'cloud':
        this.saveXML().then((xml) => {
          const defaultName = 'diagram';

          this.$q
            .dialog({
              message: `${this.$t(
                'processes.bpmnEditor.export.toCloud.message'
              ).toString()}:`,
              prompt: {
                isValid: (val: string): boolean =>
                  !!(val && val.trim().length > 2),
                model: defaultName,
              },
              title: this.$t(
                'processes.bpmnEditor.export.toCloud.title'
              ).toString(),
            })
            .onOk((response: string) => {
              this.cloud.loading = true;

              const blob: Blob = new Blob([xml], {
                type: 'text/xml',
              });
              const file: File = new File(
                [blob],
                `${response.trim() || defaultName}.bpmn`
              );

              uploadBPMN(file).finally(() => {
                this.cloud.loading = false;
              });
            });
        });

        break;
      case 'device':
        this.saveXML().then((xml) => {
          const defaultName = 'diagram';

          this.$q
            .dialog({
              message: `${this.$t(
                'processes.bpmnEditor.export.toDevice.message'
              ).toString()}:`,
              prompt: {
                isValid: (val: string): boolean =>
                  !!(val && val.trim().length > 2),
                model: defaultName,
              },
              title: this.$t(
                'processes.bpmnEditor.export.toDevice.title'
              ).toString(),
            })
            .onOk((response: string) => {
              saveFileViaLink({
                anchor: this.$refs.export as HTMLAnchorElement,
                hrefData: `data:application/xml;charset=utf-8,${encodeURIComponent(
                  xml
                )}`,
                fileName: `${response.trim() || defaultName}.bpmn`,
              });
            });
        });

        break;
    }
  }

  createHandler() {
    this.menu = false;

    this.clear().then(() => {
      this.createDiagram();
    });
  }

  protected clear(): Promise<void> {
    return new Promise((resolve, reject) => {
      let clear: (() => Promise<void>) | null = null;

      try {
        clear = async () => await this.bpmnEditor.clear();
      } catch (e) {
        return reject(e);
      }

      clear()
        .then(() => {
          return resolve();
        })
        .catch((e) => {
          return reject(e);
        });
    });
  }

  protected saveXML(): Promise<string> {
    return new Promise((resolve, reject) => {
      let saveXML:
        | ((
            options: any,
            done: (error: string, xml: string) => void
          ) => Promise<string>)
        | null = null;

      try {
        saveXML = async (
          options: {
            format?: boolean;
            preamble?: boolean;
          },
          done: (error: string, xml: string) => void
        ) => await this.bpmnEditor.saveXML(options, done);
      } catch (e) {
        return reject(e);
      }

      saveXML(
        {
          format: true,
        },
        (error, xml) => {
          if (error) {
            return reject(error);
          } else {
            return resolve(xml);
          }
        }
      );
    });
  }

  protected createDiagram(): Promise<void> {
    return new Promise((resolve, reject) => {
      let createDiagram: (() => Promise<void>) | null = null;

      try {
        createDiagram = async () => await this.bpmnEditor.createDiagram();
      } catch (e) {
        return reject(e);
      }

      createDiagram()
        .then(() => {
          return resolve();
        })
        .catch((e) => {
          return reject(e);
        });
    });
  }

  private loadTextFromFile(e: Event | MouseEvent): void {
    const element: HTMLInputElement | null =
      (e.target as HTMLInputElement) || null;

    if (element && element.files) {
      const file: File | null = element.files[0] || null;
      const type: string = (
        get(
          {
            file,
          },
          'file.name'
        ) || ''
      )
        .split('.')
        .pop();

      if (type !== 'bpmn') {
        notifyError('Поддерживаются только файлы с расширением *.bpmn');

        return;
      }

      const fileReader: FileReader = new FileReader();

      fileReader.onload = (e: ProgressEvent<FileReader>) => {
        const xml: string = (
          get(
            {
              e,
            },
            'e.target.result'
          ) || ''
        ).toString();

        this.clear().then(() => {
          this.importXML(xml);
        });
      };

      fileReader.readAsText(file);
    }
  }

  private beforeDestroy() {
    this.destroy();
  }

  private destroy(): void {
    if (this.bpmnEditor) {
      this.bpmnEditor.destroy();
    }
  }

  protected importXML(xml: string): Promise<void> {
    return new Promise((resolve, reject) => {
      let importXML: ((xml: string) => Promise<void>) | null = null;

      try {
        importXML = async (xml: string) => await this.bpmnEditor.importXML(xml);
      } catch (e) {
        return reject(e);
      }

      importXML(xml)
        .then(() => {
          return resolve();
        })
        .catch((e) => {
          return reject(e);
        });
    });
  }

  protected async mounted(): Promise<void> {
    this.init().then(() => {
      if (isEmpty(this.schema)) {
        this.createDiagram();
      } else {
        this.selectHandler(this.schema);
      }
    });
  }
}

declare interface UploaderValue {
  code: 'string';
  value: 'string' | File;
}
