/*
 * 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,
    EventEmitter,
    Input,
    OnChanges,
    Output,
    SimpleChanges,
    ViewChild,
} from '@angular/core';
import { RestService } from '../../../../services/rest.service';
import { ITreeOptions, TreeComponent } from '@circlon/angular-tree-component';
import { UUID } from 'angular2-uuid';
import { DataTypesService } from '../../../../services/data-type.service';
import {
    AdapterDescription,
    CorrectionValueTransformationRuleDescription,
    EventPropertyNested,
    EventPropertyPrimitive,
    EventPropertyUnion,
    EventSchema,
    FieldStatusInfo,
    GuessSchema,
    SpLogMessage,
} from '@streampipes/platform-services';
import { MatStepper } from '@angular/material/stepper';
import { UserErrorMessage } from '../../../../../core-model/base/UserErrorMessage';
import { TransformationRuleService } from '../../../../services/transformation-rule.service';
import { StaticValueTransformService } from '../../../../services/static-value-transform.service';
import { EventPropertyUtilsService } from '../../../../services/event-property-utils.service';

@Component({
    selector: 'sp-event-schema',
    templateUrl: './event-schema.component.html',
    styleUrls: ['./event-schema.component.scss'],
})
export class EventSchemaComponent implements OnChanges {
    constructor(
        private restService: RestService,
        private dataTypesService: DataTypesService,
        private transformationRuleService: TransformationRuleService,
        private staticValueTransformService: StaticValueTransformService,
        private epUtilsService: EventPropertyUtilsService,
    ) {}

    @Input()
    adapterDescription: AdapterDescription;

    isEditable = true;

    originalSchema: EventSchema;
    targetSchema: EventSchema = new EventSchema();
    timestampPresent = false;

    @Input()
    isEditMode;
    refreshedEventSchema = false;

    @Output()
    isEditableChange = new EventEmitter<boolean>();

    @Output()
    goBackEmitter: EventEmitter<MatStepper> = new EventEmitter();

    /**
     * Cancels the adapter configuration process
     */
    @Output()
    removeSelectionEmitter: EventEmitter<boolean> = new EventEmitter();

    /**
     * Go to next configuration step when this is complete
     */
    @Output()
    clickNextEmitter: EventEmitter<MatStepper> = new EventEmitter();

    _tree: TreeComponent;

    schemaGuess: GuessSchema = new GuessSchema();
    countSelected = 0;
    isLoading = false;
    isError = false;
    isPreviewEnabled = false;
    errorMessage: SpLogMessage;
    nodes: EventPropertyUnion[] = new Array<EventPropertyUnion>();
    validEventSchema = false;
    schemaErrorHints: UserErrorMessage[] = [];

    eventPreview: string[];
    desiredPreview: Record<string, any>;
    fieldStatusInfo: Record<string, FieldStatusInfo>;

    options: ITreeOptions = {
        childrenField: 'eventProperties',
        allowDrag: () => {
            return this.isEditable;
        },
        allowDrop: (node, { parent }) => {
            return (
                parent.data instanceof EventPropertyNested ||
                parent.data.virtual
            );
        },
        displayField: 'runTimeName',
    };

    public onUpdateData(treeComponent: TreeComponent): void {
        treeComponent.treeModel.expandAll();
    }

    public setEventSchemaEditWarning() {
        this.schemaErrorHints.push(
            new UserErrorMessage(
                'Edit mode',
                'Changes in the adapter might require you to refresh the event schema.',
                'info',
            ),
        );
    }

    public guessSchema(): void {
        this.isLoading = true;
        this.isError = false;
        this.restService.getGuessSchema(this.adapterDescription).subscribe(
            guessSchema => {
                this.eventPreview = guessSchema.eventPreview;
                this.fieldStatusInfo = guessSchema.fieldStatusInfo;
                this.targetSchema = guessSchema.targetSchema;
                this.targetSchema.eventProperties.sort((a, b) => {
                    return a.runtimeName < b.runtimeName ? -1 : 1;
                });
                this.schemaGuess = guessSchema;

                this.originalSchema = guessSchema.eventSchema;
                this.validEventSchema = this.checkIfValid(this.targetSchema);

                this.isEditable = true;
                this.isEditableChange.emit(true);
                this.isLoading = false;
                this.refreshedEventSchema = true;
                this.refreshTree();
                if (
                    guessSchema.eventPreview &&
                    guessSchema.eventPreview.length > 0
                ) {
                    this.updatePreview();
                }
            },
            errorMessage => {
                this.errorMessage = errorMessage.error;
                this.isError = true;
                this.isLoading = false;
                this.targetSchema = new EventSchema();
            },
        );
    }

    public refreshTree(refreshPreview = true): void {
        if (this.targetSchema && this.targetSchema.eventProperties) {
            this.nodes = new Array<EventPropertyUnion>();
            this.nodes.push(...this.targetSchema.eventProperties);
            this.validEventSchema = this.checkIfValid(this.targetSchema);
            if (refreshPreview) {
                this.updatePreview();
            }
            setTimeout(() => {
                if (this._tree) {
                    this._tree.treeModel.expandAll();
                }
            });
        }
    }

    public addNestedProperty(eventProperty?: EventPropertyNested): void {
        const uuid: string = UUID.UUID();
        const nested: EventPropertyNested = new EventPropertyNested();
        nested['@class'] =
            'org.apache.streampipes.model.schema.EventPropertyNested';
        nested.elementId = uuid;
        nested.eventProperties = [];
        nested.domainProperties = [];
        nested.runtimeName = 'nested';
        nested.additionalMetadata = {};
        if (!eventProperty) {
            this.targetSchema.eventProperties.push(nested);
        } else {
            eventProperty.eventProperties.push(nested);
        }
        this.refreshTree();
    }

    public removeSelectedProperties(eventProperties?: any): void {
        eventProperties = eventProperties || this.targetSchema.eventProperties;
        for (let i = eventProperties.length - 1; i >= 0; --i) {
            if (eventProperties[i].eventProperties) {
                this.removeSelectedProperties(
                    eventProperties[i].eventProperties,
                );
            }
            if (eventProperties[i].selected) {
                eventProperties.splice(i, 1);
            }
        }
        this.countSelected = 0;
        this.refreshTree();
    }

    public addStaticValueProperty(runtimeName: string): void {
        const eventProperty = new EventPropertyPrimitive();
        eventProperty['@class'] =
            'org.apache.streampipes.model.schema.EventPropertyPrimitive';
        eventProperty.elementId =
            this.staticValueTransformService.makeDefaultElementId();

        eventProperty.runtimeName = runtimeName;
        eventProperty.runtimeType = this.dataTypesService.getStringTypeUrl();
        eventProperty.domainProperties = [];
        eventProperty.propertyScope = 'DIMENSION_PROPERTY';
        eventProperty.additionalMetadata = {};

        this.targetSchema.eventProperties.push(eventProperty);
        this.refreshTree();
    }

    public addTimestampProperty(): void {
        const eventProperty = new EventPropertyPrimitive();
        eventProperty['@class'] =
            'org.apache.streampipes.model.schema.EventPropertyPrimitive';
        eventProperty.elementId =
            'http://eventProperty.de/timestamp/' + UUID.UUID();

        eventProperty.runtimeName = 'timestamp';
        eventProperty.label = 'Timestamp';
        eventProperty.description = 'The current timestamp value';
        eventProperty.domainProperties = ['http://schema.org/DateTime'];
        eventProperty.propertyScope = 'HEADER_PROPERTY';
        eventProperty.runtimeType = 'http://www.w3.org/2001/XMLSchema#long';
        eventProperty.additionalMetadata = {};

        this.targetSchema.eventProperties.push(eventProperty);
        this.refreshTree();
    }

    public updatePreview(): void {
        this.isPreviewEnabled = false;
        const ruleDescriptions =
            this.transformationRuleService.getTransformationRuleDescriptions(
                this.originalSchema,
                this.targetSchema,
            );
        if (this.eventPreview && this.eventPreview.length > 0) {
            this.restService
                .getAdapterEventPreview({
                    rules: ruleDescriptions,
                    inputData: this.eventPreview[0],
                })
                .subscribe(preview => {
                    this.desiredPreview = preview;
                    this.isPreviewEnabled = true;
                });
        }
    }

    ngOnChanges(changes: SimpleChanges) {
        setTimeout(() => {
            this.refreshTree();
        }, 200);
    }

    public removeSelection() {
        this.removeSelectionEmitter.emit();
    }

    public clickNext() {
        this.clickNextEmitter.emit();
    }

    public goBack() {
        this.goBackEmitter.emit();
    }

    private checkIfValid(eventSchema: EventSchema): boolean {
        this.timestampPresent = false;
        eventSchema.eventProperties.forEach(p => {
            if (p.domainProperties.indexOf('http://schema.org/DateTime') > -1) {
                this.timestampPresent = true;
            }
        });

        this.schemaErrorHints = [];

        if (this.isEditMode && !this.refreshedEventSchema) {
            this.setEventSchemaEditWarning();
        }

        if (!this.timestampPresent) {
            this.schemaErrorHints.push(
                new UserErrorMessage(
                    'Missing Timestamp',
                    'The timestamp must be a UNIX timestamp in milliseconds. Edit the timestamp field or add an ingestion timestamp.',
                ),
            );
        }

        if (this.fieldStatusInfo) {
            const badFields = eventSchema.eventProperties
                .filter(
                    ep => this.fieldStatusInfo[ep.runtimeName] !== undefined,
                )
                .map(ep => this.fieldStatusInfo[ep.runtimeName])
                .find(field => field.fieldStatus !== 'GOOD');
            if (badFields !== undefined) {
                this.schemaErrorHints.push(
                    new UserErrorMessage(
                        'Bad reading',
                        'At least one field could not be properly read. If this is a permanent problem, consider removing it - keeping this field might cause the adapter to fail or to omit sending events.',
                        'warning',
                    ),
                );
            }
        }

        return this.timestampPresent;
    }

    getOriginalSchema(): EventSchema {
        return this.originalSchema;
    }

    getTargetSchema(): EventSchema {
        this.targetSchema.eventProperties = this.nodes;
        return this.targetSchema;
    }

    onNodeMove(event: any) {
        this.targetSchema.eventProperties = this.nodes;
        this.updatePreview();
    }

    @ViewChild('tree')
    set tree(treeComponent: TreeComponent) {
        this._tree = treeComponent;
    }

    get tree(): TreeComponent {
        return this._tree;
    }
}
