/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import {
  CodeSpec,
  EnvironmentSpec,
  ExperimentMeta,
  ExperimentSpec,
  Specs
} from '@submarine/interfaces/experiment-spec';
import { ExperimentFormService } from '@submarine/services/experiment.form.service';
import { ExperimentService } from '@submarine/services/experiment.service';
import { ExperimentValidatorService } from '@submarine/services/experiment.validator.service';
import { customAlphabet } from 'nanoid';
import { alphanumeric } from 'nanoid-dictionary';
import { NzMessageService } from 'ng-zorro-antd';
import { Subscription } from 'rxjs';

@Component({
  selector: 'submarine-experiment-customized-form',
  templateUrl: './experiment-customized-form.component.html',
  styleUrls: ['./experiment-customized-form.component.scss']
})
export class ExperimentCustomizedFormComponent implements OnInit, OnDestroy {
  @Input() mode: 'create' | 'update' | 'clone';

  // About new experiment
  experiment: FormGroup;
  finalExperimentSpec: ExperimentSpec;
  step: number = 0;
  subscriptions: Subscription[] = [];
  listOfOption: Array<{ label: string; value: string }> = [];

  // TODO: Fetch all images from submarine server
  imageIndex = 0;
  defaultImage = 'apache/submarine:tf-mnist-with-summaries-1.0';
  imageList = [this.defaultImage];

  // Constants
  TF_SPECNAMES = ['Master', 'Worker', 'Ps'];
  PYTORCH_SPECNAMES = ['Master', 'Worker'];
  XGBOOST_SPECNAMES = ['Master', 'Worker'];
  defaultSpecName = 'worker';
  MEMORY_UNITS = ['M', 'G'];

  SECOND_STEP = 1;
  PREVIEW_STEP = 2;
  ADVANCED = false;

  // About env page
  currentEnvPage = 1;
  PAGESIZE = 5;

  // About spec
  jobTypes = 'Distributed Tensorflow';
  framework = 'Tensorflow';
  currentSpecPage = 1;

  // About update
  @Input() targetId: string = null;
  @Input() targetSpec: ExperimentSpec = null;

  constructor(
    private experimentService: ExperimentService,
    private experimentValidatorService: ExperimentValidatorService,
    private experimentFormService: ExperimentFormService,
    private nzMessageService: NzMessageService
  ) {}

  ngOnInit() {
    this.experiment = new FormGroup({
      experimentName: new FormControl(null, [Validators.pattern('([a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]|[a-zA-Z0-9]+)'), Validators.required]),
      description: new FormControl(null, [Validators.required]),
      tags: new FormControl([], []),
      cmd: new FormControl('', [Validators.required]),
      image: new FormControl(this.defaultImage, [Validators.required]),
      envs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('key')]),
      specs: new FormArray([], [this.experimentValidatorService.nameValidatorFactory('name')]),
      gitRepo: new FormControl(null, []),
      gitBranch: new FormControl(null, []),
      gitUsername: new FormControl(null, []),
      gitPassword: new FormControl(null, [])
    });

    // Initialise the list of image, the default values of image are from the current experiment list images
    this.experimentService.fetchExperimentList().subscribe(
      (list) => {
        list.forEach((item) => {
          const envImage = item.spec.environment.image;
          if (this.imageList.indexOf(envImage) === -1) {
            this.imageList.push(envImage)
          }
        });
      },
      (error) => {
        console.error(error);
      }
    )

    // Bind the component method for callback
    this.checkStatus = this.checkStatus.bind(this);

    if (this.mode === 'update') {
      this.updateExperimentInit();
    } else if (this.mode === 'clone') {
      this.cloneExperimentInit(this.targetSpec);
    }

    // Fire status to parent when form value has changed
    const sub1 = this.experiment.valueChanges.subscribe(this.checkStatus);

    const sub2 = this.experimentFormService.stepService.subscribe((n) => {
      if (n > 0) {
        if (this.step === this.PREVIEW_STEP) {
          this.handleSubmit();
        } else if (this.step === this.SECOND_STEP) {
          this.onPreview();
          this.step += 1;
        } else {
          this.step += 1;
        }
      } else {
        this.step -= 1;
      }
      // Send the current step and okText back to parent
      this.experimentFormService.modalPropsChange({
        okText: this.step !== this.PREVIEW_STEP ? 'Next step' : 'Submit',
        currentStep: this.step
      });
      // Run check after step is changed
      this.checkStatus();
    });

    this.subscriptions.push(sub1, sub2);

    //TODO: get tags from server
    this.listOfOption = [];

  }

  ngOnDestroy() {
    // Clean up the subscriptions
    this.subscriptions.forEach((sub) => {
      sub.unsubscribe();
    });
  }

  addItem(input: HTMLInputElement): void {
    let value = input.value;
    if (value === null || typeof value === "undefined") {
      return;
    } else {
      value = value.replace(/\s+/g, "");
      if (value !== "" && this.imageList.indexOf(value) === -1) {
        this.imageList = [...this.imageList, value];
      }
    }
  }

  // Getters of experiment request form
  get experimentName() {
    return this.experiment.get('experimentName');
  }
  get tags() {
    return this.experiment.get('tags');
  }
  get description() {
    return this.experiment.get('description');
  }
  get cmd() {
    return this.experiment.get('cmd');
  }
  get envs() {
    return this.experiment.get('envs') as FormArray;
  }
  get image() {
    return this.experiment.get('image');
  }
  get specs() {
    return this.experiment.get('specs') as FormArray;
  }
  get gitRepo() {
    return this.experiment.get('gitRepo');
  }
  get gitBranch() {
    return this.experiment.get('gitBranch');
  }
  get gitGitUsername() {
    return this.experiment.get('gitUsername');
  }
  get gitGitPassword() {
    return this.experiment.get('gitPassword');
  }

  /**
   * Reset properties in parent component when the form is about to closed
   */
  closeModal() {
    this.experimentFormService.modalPropsClear();
  }

  /**
   * Check the validity of the experiment page
   */
  checkStatus() {
    if (this.step === 0) {
      this.experimentFormService.btnStatusChange(
        this.experimentName.invalid ||
          this.tags.invalid ||
          this.cmd.invalid ||
          this.image.invalid ||
          this.envs.invalid
      );
    } else if (this.step === 1) {
      this.experimentFormService.btnStatusChange(this.specs.invalid);
    }
  }
  onPreview() {
    this.finalExperimentSpec = this.constructSpec();
  }
  /**
   * Event handler for Next step/Submit button
   */
  handleSubmit() {
    if (this.mode === 'create') {
      this.experimentService.createExperiment(this.finalExperimentSpec).subscribe({
        next: () => {},
        error: (msg) => {
          this.nzMessageService.error(`${msg}, please try again`, {
            nzPauseOnHover: true
          });
        },
        complete: () => {
          this.nzMessageService.success('Experiment creation succeeds');
          this.experimentFormService.fetchList();
          this.closeModal();
        }
      });
    } else if (this.mode === 'update') {
      this.experimentService.updateExperiment(this.targetId, this.finalExperimentSpec).subscribe(
        null,
        (msg) => {
          this.nzMessageService.error(`${msg}, please try again`, {
            nzPauseOnHover: true
          });
        },
        () => {
          this.nzMessageService.success('Modification succeeds!');
          this.experimentFormService.fetchList();
          this.closeModal();
        }
      );
    } else if (this.mode === 'clone') {
      this.experimentService.createExperiment(this.finalExperimentSpec).subscribe(
        null,
        (msg) => {
          this.nzMessageService.error(`${msg}, please try again`, {
            nzPauseOnHover: true
          });
        },
        () => {
          this.nzMessageService.success('Create a new experiment !');
          this.experimentFormService.fetchList();
          this.closeModal();
        }
      );
    }
  }

  /**
   * Create a new env variable input
   */
  createEnv(defaultKey: string = '', defaultValue: string = '') {
    // Create a new FormGroup
    return new FormGroup(
      {
        key: new FormControl(defaultKey, [Validators.required]),
        value: new FormControl(defaultValue, [Validators.required])
      },
      [this.experimentValidatorService.envValidator]
    );
  }
  /**
   * Create a new spec
   */
  createSpec(
    defaultName: string = 'Worker',
    defaultReplica: number = 1,
    defaultCpu: number = 1,
    defaultGpu: number = 0,
    defaultMemory: number = 1024,
    defaultUnit: string = 'M'
  ): FormGroup {
    return new FormGroup(
      {
        name: new FormControl(defaultName, [Validators.required]),
        replicas: new FormControl(defaultReplica, [Validators.min(1), Validators.required]),
        cpus: new FormControl(defaultCpu, [Validators.min(1), Validators.required]),
        gpus: new FormControl(defaultGpu, [Validators.min(0), Validators.required]),
        memory: new FormGroup(
          {
            num: new FormControl(defaultMemory, [Validators.required]),
            unit: new FormControl(defaultUnit, [Validators.required])
          },
          [this.experimentValidatorService.memoryValidator]
        )
      },
      [this.experimentValidatorService.specValidator]
    );
  }

  /**
   * Handler for the create env button
   */
  onCreateEnv() {
    const env = this.createEnv();
    this.envs.push(env);
    // If the new page is created, jump to that page
    if (this.envs.controls.length > 1 && this.envs.controls.length % this.PAGESIZE === 1) {
      this.currentEnvPage += 1;
    }
  }

  /**
   * Handler for the create spec button
   */
  onCreateSpec() {
    const spec = this.createSpec();
    this.specs.push(spec);
    // If the new page is created, jump to that page
    if (this.specs.controls.length > 1 && this.specs.controls.length % this.PAGESIZE === 1) {
      this.currentSpecPage += 1;
    }
  }

  /**
   * Construct spec for new experiment creation
   */
  constructSpec(): ExperimentSpec {
    // Construct the spec
    const meta: ExperimentMeta = {
      name: this.experimentName.value.toLowerCase(),
      tags: this.tags.value,
      framework: this.framework === 'Standalone' ? 'Tensorflow' : this.framework,
      cmd: this.cmd.value,
      envVars: {},
    };
    for (const env of this.envs.controls) {
      if (env.get('key').value) {
        meta.envVars[env.get('key').value] = env.get('value').value;
      }
    }

    const specs: Specs = {};
    for (const spec of this.specs.controls) {
      if (spec.get('name').value) {
        specs[spec.get('name').value] = {
          replicas: spec.get('replicas').value,
          resources: `cpu=${spec.get('cpus').value},nvidia.com/gpu=${spec.get('gpus').value},memory=${
            spec.get('memory').get('num').value
          }${spec.get('memory').get('unit').value}`
        };
      }
    }

    const environment: EnvironmentSpec = {
      image: this.image.value
    };

    const code: CodeSpec = {
      syncMode: 'git',
      git: {
        url: this.gitRepo.value,
        branch: this.gitBranch.value,
        username: this.gitGitUsername.value,
        password: this.gitGitPassword.value
      }
    };

    const newExperimentSpec: ExperimentSpec = {
      meta: meta,
      environment: environment,
      spec: specs
    };

    if (code.git.url !== null) {
      newExperimentSpec.code = code;
    }
    return newExperimentSpec;
  }

  /**
   * Delete list items(envs or specs)
   *
   * @param arr - The FormArray containing the item
   * @param index - The index of the item
   */
  deleteItem(arr: FormArray, index: number) {
    arr.removeAt(index);
  }

  deleteAllItem(arr: FormArray) {
    arr.clear();
  }

  updateExperimentInit() {
    // Prevent user from modifying the name
    this.experimentName.disable();
    // Put value back
    this.experimentName.setValue(this.targetSpec.meta.name);
    this.cloneExperiment(this.targetSpec);
    // Check status to enable next btn
    this.checkStatus();
  }

  cloneExperimentInit(spec: ExperimentSpec) {
    // Enable user from modifying the name
    this.experimentName.enable();
    // Put value back
    const id: string = customAlphabet(alphanumeric, 8)();
    const cloneExperimentName = spec.meta.name + '-' + id;
    this.experimentName.setValue(cloneExperimentName.toLocaleLowerCase());
    this.cloneExperiment(spec);
    this.checkStatus();
  }

  cloneExperiment(spec: ExperimentSpec) {
    this.tags.setValue(spec.meta.tags);
    this.description.setValue(spec.meta.description);
    this.cmd.setValue(spec.meta.cmd);
    this.image.setValue(spec.environment.image);
    if (this.imageList.indexOf(spec.environment.image) === -1) {
      this.imageList = [...this.imageList, spec.environment.image || `New item ${this.imageIndex++}`];
    }
    for (const [key, value] of Object.entries(spec.meta.envVars)) {
      const env = this.createEnv(key, value);
      this.envs.push(env);
    }
    for (const [specName, info] of Object.entries(spec.spec)) {
      const cpuCount = info.resourceMap.cpu;
      const gpuCount = info.resourceMap.gpu === undefined ? '0' : '1';
      const [memory, unit] = info.resourceMap.memory.match(/\d+|[MG]/g);
      const newSpec = this.createSpec(
        specName,
        parseInt(info.replicas, 10),
        parseInt(cpuCount, 10),
        parseInt(gpuCount, 10),
        parseInt(memory, 10),
        unit
      );
      this.specs.push(newSpec);
    }
  }
}
