/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed 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 '@polymer/iron-input/iron-input';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
import '../gr-permission/gr-permission';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
import {htmlTemplate} from './gr-access-section_html';
import {
  AccessPermissions,
  PermissionArray,
  PermissionArrayItem,
  toSortedPermissionsArray,
} from '../../../utils/access-util';
import {customElement, property} from '@polymer/decorators';
import {
  EditablePermissionInfo,
  PermissionAccessSection,
  EditableProjectAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {
  CapabilityInfoMap,
  GitRef,
  LabelNameToLabelTypeInfoMap,
} from '../../../types/common';
import {PolymerDomRepeatEvent} from '../../../types/types';

/**
 * Fired when the section has been modified or removed.
 *
 * @event access-modified
 */

/**
 * Fired when a section that was previously added was removed.
 *
 * @event added-section-removed
 */

const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';

// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';
const ON_BEHALF_OF = '(On Behalf Of)';
const LABEL = 'Label';

export interface GrAccessSection {
  $: {
    permissionSelect: HTMLSelectElement;
  };
}

@customElement('gr-access-section')
export class GrAccessSection extends GestureEventListeners(
  LegacyElementMixin(PolymerElement)
) {
  static get template() {
    return htmlTemplate;
  }

  @property({type: Object})
  capabilities?: CapabilityInfoMap;

  @property({type: Object, notify: true, observer: '_updateSection'})
  section?: PermissionAccessSection;

  @property({type: Object})
  groups?: EditableProjectAccessGroups;

  @property({type: Object})
  labels?: LabelNameToLabelTypeInfoMap;

  @property({type: Boolean, observer: '_handleEditingChanged'})
  editing = false;

  @property({type: Boolean})
  canUpload?: boolean;

  @property({type: Array})
  ownerOf?: GitRef[];

  @property({type: String})
  _originalId?: GitRef;

  @property({type: Boolean})
  _editingRef = false;

  @property({type: Boolean})
  _deleted = false;

  @property({type: Array})
  _permissions?: PermissionArray<EditablePermissionInfo>;

  /** @override */
  created() {
    super.created();
    this.addEventListener('access-saved', () => this._handleAccessSaved());
  }

  _updateSection(section: PermissionAccessSection) {
    this._permissions = toSortedPermissionsArray(section.value.permissions);
    this._originalId = section.id as GitRef;
  }

  _handleAccessSaved() {
    if (!this.section) {
      return;
    }
    // Set a new 'original' value to keep track of after the value has been
    // saved.
    this._updateSection(this.section);
  }

  _handleValueChange() {
    if (!this.section) {
      return;
    }
    if (!this.section.value.added) {
      this.section.value.modified = this.section.id !== this._originalId;
      // Allows overall access page to know a change has been made.
      // For a new section, this is not fired because new permissions and
      // rules have to be added in order to save, modifying the ref is not
      // enough.
      this.dispatchEvent(
        new CustomEvent('access-modified', {bubbles: true, composed: true})
      );
    }
    this.section.value.updatedId = this.section.id;
  }

  _handleEditingChanged(editing: boolean, editingOld: boolean) {
    // Ignore when editing gets set initially.
    if (!editingOld) {
      return;
    }
    if (!this.section || !this._permissions) {
      return;
    }
    // Restore original values if no longer editing.
    if (!editing) {
      this._editingRef = false;
      this._deleted = false;
      delete this.section.value.deleted;
      // Restore section ref.
      this.set(['section', 'id'], this._originalId);
      // Remove any unsaved but added permissions.
      this._permissions = this._permissions.filter(p => !p.value.added);
      for (const key of Object.keys(this.section.value.permissions)) {
        if (this.section.value.permissions[key].added) {
          delete this.section.value.permissions[key];
        }
      }
    }
  }

  _computePermissions(
    name: string,
    capabilities?: CapabilityInfoMap,
    labels?: LabelNameToLabelTypeInfoMap
  ) {
    let allPermissions;
    const section = this.section;
    if (!section || !section.value) {
      return [];
    }
    if (name === GLOBAL_NAME) {
      allPermissions = toSortedPermissionsArray(capabilities);
    } else {
      const labelOptions = this._computeLabelOptions(labels);
      allPermissions = labelOptions.concat(
        toSortedPermissionsArray(AccessPermissions)
      );
    }
    return allPermissions.filter(
      permission => !section.value.permissions[permission.id]
    );
  }

  _computeHideEditClass(section: PermissionAccessSection) {
    return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
  }

  _handleAddedPermissionRemoved(e: PolymerDomRepeatEvent) {
    if (!this._permissions) {
      return;
    }
    const index = e.model.index;
    this._permissions = this._permissions
      .slice(0, index)
      .concat(this._permissions.slice(index + 1, this._permissions.length));
  }

  _computeLabelOptions(labels?: LabelNameToLabelTypeInfoMap) {
    const labelOptions = [];
    if (!labels) {
      return [];
    }
    for (const labelName of Object.keys(labels)) {
      labelOptions.push({
        id: 'label-' + labelName,
        value: {
          name: `${LABEL} ${labelName}`,
          id: 'label-' + labelName,
        },
      });
      labelOptions.push({
        id: 'labelAs-' + labelName,
        value: {
          name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
          id: 'labelAs-' + labelName,
        },
      });
    }
    return labelOptions;
  }

  _computePermissionName(
    name: string,
    permission: PermissionArrayItem<EditablePermissionInfo>,
    capabilities: CapabilityInfoMap
  ) {
    if (name === GLOBAL_NAME) {
      return capabilities[permission.id].name;
    } else if (AccessPermissions[permission.id]) {
      return AccessPermissions[permission.id].name;
    } else if (permission.value.label) {
      let behalfOf = '';
      if (permission.id.startsWith('labelAs-')) {
        behalfOf = ON_BEHALF_OF;
      }
      return `${LABEL} ${permission.value.label}${behalfOf}`;
    }
    return undefined;
  }

  _computeSectionName(name: string) {
    // When a new section is created, it doesn't yet have a ref. Set into
    // edit mode so that the user can input one.
    if (!name) {
      this._editingRef = true;
      // Needed for the title value. This is the same default as GWT.
      name = NEW_NAME;
      // Needed for the input field value.
      this.set('section.id', name);
    }
    if (name === GLOBAL_NAME) {
      return 'Global Capabilities';
    } else if (name.startsWith(REFS_NAME)) {
      return `Reference: ${name}`;
    }
    return name;
  }

  _handleRemoveReference() {
    if (!this.section) {
      return;
    }
    if (this.section.value.added) {
      this.dispatchEvent(
        new CustomEvent('added-section-removed', {
          bubbles: true,
          composed: true,
        })
      );
    }
    this._deleted = true;
    this.section.value.deleted = true;
    this.dispatchEvent(
      new CustomEvent('access-modified', {bubbles: true, composed: true})
    );
  }

  _handleUndoRemove() {
    if (!this.section) {
      return;
    }
    this._deleted = false;
    delete this.section.value.deleted;
  }

  editRefInput() {
    return this.root!.querySelector(
      PolymerElement
        ? 'iron-input.editRefInput'
        : 'input[is=iron-input].editRefInput'
    ) as HTMLInputElement;
  }

  editReference() {
    this._editingRef = true;
    this.editRefInput().focus();
  }

  _isEditEnabled(
    canUpload: boolean | undefined,
    ownerOf: GitRef[] | undefined,
    sectionId: GitRef
  ) {
    return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
  }

  _computeSectionClass(
    editing: boolean,
    canUpload: boolean | undefined,
    ownerOf: GitRef[] | undefined,
    editingRef: boolean,
    deleted: boolean
  ) {
    const classList = [];
    if (
      editing &&
      this.section &&
      this._isEditEnabled(canUpload, ownerOf, this.section.id as GitRef)
    ) {
      classList.push('editing');
    }
    if (editingRef) {
      classList.push('editingRef');
    }
    if (deleted) {
      classList.push('deleted');
    }
    return classList.join(' ');
  }

  _computeEditBtnClass(name: string) {
    return name === GLOBAL_NAME ? 'global' : '';
  }

  _handleAddPermission() {
    const value = this.$.permissionSelect.value;
    const permission: PermissionArrayItem<EditablePermissionInfo> = {
      id: value,
      value: {rules: {}, added: true},
    };

    // This is needed to update the 'label' property of the
    // 'label-<label-name>' permission.
    //
    // The value from the add permission dropdown will either be
    // label-<label-name> or labelAs-<labelName>.
    // But, the format of the API response is as such:
    // "permissions": {
    //  "label-Code-Review": {
    //    "label": "Code-Review",
    //    "rules": {...}
    //    }
    //  }
    // }
    // When we add a new item, we have to push the new permission in the same
    // format as the ones that have been returned by the API.
    if (value.startsWith('label')) {
      permission.value.label = value
        .replace('label-', '')
        .replace('labelAs-', '');
    }
    // Add to the end of the array (used in dom-repeat) and also to the
    // section object that is two way bound with its parent element.
    this.push('_permissions', permission);
    this.set(['section.value.permissions', permission.id], permission.value);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'gr-access-section': GrAccessSection;
  }
}
