import { Injectable } from '@angular/core';

import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
import { findIndex, findKey, isEqual } from 'lodash';

import { SubmissionState } from '../submission.reducers';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
import {
  DisableSectionAction,
  EnableSectionAction,
  InertSectionErrorsAction,
  RemoveSectionErrorsAction,
  SectionStatusChangeAction,
  SetSectionFormId,
  UpdateSectionDataAction
} from '../objects/submission-objects.actions';
import {
  SubmissionObjectEntry
} from '../objects/submission-objects.reducer';
import {
  submissionObjectFromIdSelector,
  submissionSectionDataFromIdSelector,
  submissionSectionErrorsFromIdSelector,
  submissionSectionFromIdSelector,
  submissionSectionServerErrorsFromIdSelector
} from '../selectors';
import { SubmissionScopeType } from '../../core/submission/submission-scope-type';
import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths';
import { FormClearErrorsAction } from '../../shared/form/form.actions';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { SubmissionService } from '../submission.service';
import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model';
import { SectionsType } from './sections-type';
import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service';
import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model';
import { parseReviver } from '@ng-dynamic-forms/core';
import { FormService } from '../../shared/form/form.service';
import { JsonPatchOperationPathCombiner } from '../../core/json-patch/builder/json-patch-operation-path-combiner';
import { FormError } from '../../shared/form/form.reducer';
import { SubmissionSectionObject } from '../objects/submission-section-object.model';
import { SubmissionSectionError } from '../objects/submission-section-error.model';

/**
 * A service that provides methods used in submission process.
 */
@Injectable()
export class SectionsService {

  /**
   * Initialize service variables
   * @param {FormService} formService
   * @param {NotificationsService} notificationsService
   * @param {ScrollToService} scrollToService
   * @param {SubmissionService} submissionService
   * @param {Store<SubmissionState>} store
   * @param {TranslateService} translate
   */
  constructor(private formService: FormService,
              private notificationsService: NotificationsService,
              private scrollToService: ScrollToService,
              private submissionService: SubmissionService,
              private store: Store<SubmissionState>,
              private translate: TranslateService) {
  }

  /**
   * Compare the list of the current section errors with the previous one,
   * and dispatch actions to add/remove to/from the section state
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The workspaceitem self url
   * @param formId
   *    The [SubmissionDefinitionsModel] that define submission configuration
   * @param currentErrors
   *    The [SubmissionSectionError] that define submission sections init data
   * @param prevErrors
   *    The [SubmissionSectionError] that define submission sections init errors
   */
  public checkSectionErrors(
    submissionId: string,
    sectionId: string,
    formId: string,
    currentErrors: SubmissionSectionError[],
    prevErrors: SubmissionSectionError[] = []) {
    // Remove previous error list if the current is empty
    if (isEmpty(currentErrors)) {
      this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
      this.store.dispatch(new FormClearErrorsAction(formId));
    } else if (!isEqual(currentErrors, prevErrors)) { // compare previous error list with the current one
      const dispatchedErrors = [];

      // Iterate over the current error list
      currentErrors.forEach((error: SubmissionSectionError) => {
        const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);

        errorPaths.forEach((path: SectionErrorPath) => {
          if (path.fieldId) {
            // Dispatch action to add form error to the state;
            this.formService.addError(formId, path.fieldId, path.fieldIndex, error.message);
            dispatchedErrors.push(path.fieldId);
          }
        });
      });

      // Itereate over the previous error list
      prevErrors.forEach((error: SubmissionSectionError) => {
        const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path);

        errorPaths.forEach((path: SectionErrorPath) => {
          if (path.fieldId) {
            if (!dispatchedErrors.includes(path.fieldId)) {
              // Dispatch action to remove form error from the state;
              this.formService.removeError(formId, path.fieldId, path.fieldIndex);
            }
          }
        });
      });
    }
  }

  /**
   * Dispatch a new [RemoveSectionErrorsAction]
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   */
  public dispatchRemoveSectionErrors(submissionId, sectionId) {
    this.store.dispatch(new RemoveSectionErrorsAction(submissionId, sectionId));
  }

  /**
   * Dispatch a new [SetSectionFormId]
   *    The submission id
   * @param sectionId
   *    The section id
   * @param formId
   *    The form id
   */
  public dispatchSetSectionFormId(submissionId, sectionId, formId) {
    this.store.dispatch(new SetSectionFormId(submissionId, sectionId, formId));
  }

  /**
   * Return the data object for the specified section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param sectionType
   *    The type of section to retrieve
   * @return Observable<WorkspaceitemSectionDataType>
   *    observable of [WorkspaceitemSectionDataType]
   */
  public getSectionData(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<WorkspaceitemSectionDataType> {
    return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe(
      map((sectionData: WorkspaceitemSectionDataType) => {
        if (sectionType === SectionsType.SubmissionForm) {
          return normalizeSectionData(sectionData);
        } else {
          return sectionData;
        }
      }),
      distinctUntilChanged(),
    );
  }

  /**
   * Get the list of validation errors present in the given section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param sectionType
   *    The type of section for which retrieve errors
   */
  getShownSectionErrors(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<SubmissionSectionError[]> {
    let errorsState$: Observable<SubmissionSectionError[]>;
    if (sectionType !== SectionsType.SubmissionForm) {
      errorsState$ = this.getSectionErrors(submissionId, sectionId);
    } else {
      errorsState$ = this.getSectionState(submissionId, sectionId, sectionType).pipe(
        mergeMap((state: SubmissionSectionObject) => this.formService.getFormErrors(state.formId).pipe(
          map((formErrors: FormError[]) => {
            const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId);
            const sectionErrors = formErrors
              .map((error) => ({
                path: pathCombiner.getPath(error.fieldId.replace(/\_/g, '.')).path,
                message: error.message
              } as SubmissionSectionError))
              .filter((sectionError: SubmissionSectionError) => findIndex(state.errorsToShow, {path: sectionError.path}) === -1);
            return [...state.errorsToShow, ...sectionErrors];
          })
        ))
      );
    }

    return errorsState$;
  }
  /**
   * Return the error list to show for the specified section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<SubmissionSectionError>
   *    observable of array of [SubmissionSectionError]
   */
  public getSectionErrors(submissionId: string, sectionId: string): Observable<SubmissionSectionError[]> {
    return this.store.select(submissionSectionErrorsFromIdSelector(submissionId, sectionId)).pipe(
      distinctUntilChanged());
  }

  /**
   * Return the error list detected by the server for the specified section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<SubmissionSectionError>
   *    observable of array of [SubmissionSectionError]
   */
  public getSectionServerErrors(submissionId: string, sectionId: string): Observable<SubmissionSectionError[]> {
    return this.store.select(submissionSectionServerErrorsFromIdSelector(submissionId, sectionId)).pipe(
      distinctUntilChanged());
  }

  /**
   * Return the state object for the specified section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param sectionType
   *    The type of section to retrieve
   * @return Observable<SubmissionSectionObject>
   *    observable of [SubmissionSectionObject]
   */
  public getSectionState(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<SubmissionSectionObject> {
    return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
      filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)),
      map((sectionObj: SubmissionSectionObject) => sectionObj),
      map((sectionState: SubmissionSectionObject) => {
        if (hasValue(sectionState.data) && sectionType === SectionsType.SubmissionForm) {
          return Object.assign({}, sectionState, {
            data: normalizeSectionData(sectionState.data)
          });
        } else {
          return sectionState;
        }
      }),
      distinctUntilChanged()
      );
  }

  /**
   * Check if a given section is valid
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<boolean>
   *    Emits true whenever a given section should be valid
   */
  public isSectionValid(submissionId: string, sectionId: string): Observable<boolean> {
    return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
      filter((sectionObj) => hasValue(sectionObj)),
      map((sectionObj: SubmissionSectionObject) => sectionObj.isValid),
      distinctUntilChanged());
  }

  /**
   * Check if a given section is active
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<boolean>
   *    Emits true whenever a given section should be active
   */
  public isSectionActive(submissionId: string, sectionId: string): Observable<boolean> {
    return this.submissionService.getActiveSectionId(submissionId).pipe(
      map((activeSectionId: string) => sectionId === activeSectionId),
      distinctUntilChanged());
  }

  /**
   * Check if a given section is enabled
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<boolean>
   *    Emits true whenever a given section should be enabled
   */
  public isSectionEnabled(submissionId: string, sectionId: string): Observable<boolean> {
    return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
      filter((sectionObj) => hasValue(sectionObj)),
      map((sectionObj: SubmissionSectionObject) => sectionObj.enabled),
      distinctUntilChanged());
  }

  /**
   * Check if a given section is a read only section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param submissionScope
   *    The submission scope
   * @return Observable<boolean>
   *    Emits true whenever a given section should be read only
   */
  public isSectionReadOnly(submissionId: string, sectionId: string, submissionScope: SubmissionScopeType): Observable<boolean> {
    return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe(
      filter((sectionObj) => hasValue(sectionObj)),
      map((sectionObj: SubmissionSectionObject) => {
        return isNotEmpty(sectionObj.visibility)
          && sectionObj.visibility.other === 'READONLY'
          && submissionScope !== SubmissionScopeType.WorkspaceItem;
      }),
      distinctUntilChanged());
  }

  /**
   * Check if a given section id is present in the list of sections
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @return Observable<boolean>
   *    Emits true whenever a given section id should be available
   */
  public isSectionAvailable(submissionId: string, sectionId: string): Observable<boolean> {
    return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
      filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
      map((submissionState: SubmissionObjectEntry) => {
        return isNotUndefined(submissionState.sections) && isNotUndefined(submissionState.sections[sectionId]);
      }),
      distinctUntilChanged());
  }

  /**
   * Check if a given section type is present in the list of sections
   *
   * @param submissionId
   *    The submission id
   * @param sectionType
   *    The section type
   * @return Observable<boolean>
   *    Emits true whenever a given section type should be available
   */
  public isSectionTypeAvailable(submissionId: string, sectionType: SectionsType): Observable<boolean> {
    return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
      filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
      map((submissionState: SubmissionObjectEntry) => {
        return isNotUndefined(submissionState.sections) && isNotUndefined(findKey(submissionState.sections, {sectionType: sectionType}));
      }),
      distinctUntilChanged());
  }

  /**
   * Check if given section id is of a given section type
   * @param submissionId
   * @param sectionId
   * @param sectionType
   */
  public isSectionType(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<boolean> {
    return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
      filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
      map((submissionState: SubmissionObjectEntry) => {
        return isNotUndefined(submissionState.sections) && isNotUndefined(submissionState.sections[sectionId])
          && submissionState.sections[sectionId].sectionType === sectionType;
      }),
      distinctUntilChanged());
  }

  /**
   * Dispatch a new [EnableSectionAction] to add a new section and move page target to it
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   */
  public addSection(submissionId: string, sectionId: string) {
    this.store.dispatch(new EnableSectionAction(submissionId, sectionId));
    const config: ScrollToConfigOptions = {
      target: sectionId,
      offset: -70
    };

    this.scrollToService.scrollTo(config);
  }

  /**
   * Dispatch a new [DisableSectionAction] to remove section
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   */
  public removeSection(submissionId: string, sectionId: string) {
    this.store.dispatch(new DisableSectionAction(submissionId, sectionId));
  }

  /**
   * Dispatch a new [UpdateSectionDataAction] to update section state with new data and errors
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param data
   *    The section data
   * @param errorsToShow
   *    The list of the section's errors to show. It contains the error list
   *    to display when section is not pristine
   * @param serverValidationErrors
   *    The list of the section's errors detected by the server.
   *    They may not be shown yet if section is pristine
   * @param metadata
   *    The section metadata
   */
  public updateSectionData(
    submissionId: string,
    sectionId: string,
    data: WorkspaceitemSectionDataType,
    errorsToShow: SubmissionSectionError[] = [],
    serverValidationErrors: SubmissionSectionError[] = [],
    metadata?: string[]
  ) {
    if (isNotEmpty(data)) {
      const isAvailable$ = this.isSectionAvailable(submissionId, sectionId);
      const isEnabled$ = this.isSectionEnabled(submissionId, sectionId);

      combineLatest(isAvailable$, isEnabled$).pipe(
        take(1),
        filter(([available, enabled]: [boolean, boolean]) => available))
        .subscribe(([available, enabled]: [boolean, boolean]) => {
          this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errorsToShow, serverValidationErrors, metadata));
        });
    }
  }

  /**
   * Dispatch a new [InertSectionErrorsAction] to update section state with new error
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param error
   *    The section error
   */
  public setSectionError(submissionId: string, sectionId: string, error: SubmissionSectionError) {
    this.store.dispatch(new InertSectionErrorsAction(submissionId, sectionId, error));
  }

  /**
   * Dispatch a new [SectionStatusChangeAction] to update section state with new status
   *
   * @param submissionId
   *    The submission id
   * @param sectionId
   *    The section id
   * @param status
   *    The section status
   */
  public setSectionStatus(submissionId: string, sectionId: string, status: boolean) {
    this.store.dispatch(new SectionStatusChangeAction(submissionId, sectionId, status));
  }

  /**
   * Compute the list of selectable metadata for the section configuration.
   * @param formConfig
   */
  public computeSectionConfiguredMetadata(formConfig: string | SubmissionFormsModel): string[] {
    const metadata = [];
    const rawData = typeof formConfig === 'string' ? JSON.parse(formConfig, parseReviver) : formConfig;
    if (rawData.rows && !isEmpty(rawData.rows)) {
      rawData.rows.forEach((currentRow) => {
        if (currentRow.fields && !isEmpty(currentRow.fields)) {
          currentRow.fields.forEach((field) => {
            if (field.selectableMetadata && !isEmpty(field.selectableMetadata)) {
              field.selectableMetadata.forEach((selectableMetadata) => {
                if (!metadata.includes(selectableMetadata.metadata)) {
                  metadata.push(selectableMetadata.metadata);
                }
              });
            }
          });
        }
      });
    }
    return metadata;
  }

}
