/**
 * @license
 * Copyright 2016 Palantir Technologies, Inc.
 *
 * 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 * as update from 'immutability-helper';
import * as _ from 'lodash';
import { MosaicDropTargetPosition } from './internalTypes';
import { getAndAssertNodeAtPathExists, getOtherBranch } from './mosaicUtilities';
import {
    MosaicBranch,
    MosaicDirection,
    MosaicNode,
    MosaicParent,
    MosaicPath,
    MosaicUpdate,
    MosaicUpdateSpec,
} from './types';

// https://github.com/Microsoft/TypeScript/issues/9944
let FIX_9944: MosaicParent<any>;
FIX_9944 = null!;

/**
 * Used to prepare `update` for `immutability-helper`
 * @param mosaicUpdate
 * @returns {any}
 */
export function buildSpecFromUpdate<T>(mosaicUpdate: MosaicUpdate<T>): MosaicUpdateSpec<T> {
    if (mosaicUpdate.path.length > 0) {
        return _.set({}, mosaicUpdate.path, mosaicUpdate.spec);
    } else {
        return mosaicUpdate.spec;
    }
}

/**
 * Applies `updates` to `root`
 * @param root
 * @param updates
 * @returns {MosaicNode<T>}
 */
export function updateTree<T>(root: MosaicNode<T>, updates: MosaicUpdate<T>[]) {
    let currentNode = root;
    updates.forEach((mUpdate: MosaicUpdate<T>) => {
        currentNode = update(currentNode, buildSpecFromUpdate(mUpdate));
    });

    return currentNode;
}

/**
 * Creates a `MosaicUpdate<T>` to remove the node at `path` from `root`
 * @param root
 * @param path
 * @returns {{path: T[], spec: {$set: MosaicNode<T>}}}
 */
export function createRemoveUpdate<T>(root: MosaicNode<T> | null, path: MosaicPath): MosaicUpdate<T> {
    const parentPath = _.dropRight(path);
    const nodeToRemove = _.last(path);
    const siblingPath = parentPath.concat(getOtherBranch(nodeToRemove!));
    const sibling = getAndAssertNodeAtPathExists(root, siblingPath);

    return {
        path: parentPath,
        spec: {
            $set: sibling,
        },
    };
}

function isPathPrefixEqual(a: MosaicPath, b: MosaicPath, length: number) {
    return _.isEqual(_.take(a, length), _.take(b, length));
}

/**
 * Creates a `MosaicUpdate<T>` to split the _leaf_ at `destinationPath` into a node of it and the node from `sourcePath`
 * placing the node from `sourcePath` in `position`.
 * @param root
 * @param sourcePath
 * @param destinationPath
 * @param position
 * @returns {(MosaicUpdate<T>|{path: MosaicPath, spec: {$set: {first: MosaicNode<T>, second: MosaicNode<T>, direction: MosaicDirection}}})[]}
 */
export function createDragToUpdates<T>(root: MosaicNode<T>,
                                       sourcePath: MosaicPath,
                                       destinationPath: MosaicPath,
                                       position: MosaicDropTargetPosition): MosaicUpdate<T>[] {
    let destinationNode = getAndAssertNodeAtPathExists(root, destinationPath);
    const updates: MosaicUpdate<T>[] = [];

    const destinationIsParentOfSource = isPathPrefixEqual(sourcePath, destinationPath, destinationPath.length);
    if (destinationIsParentOfSource) {
        // Must explicitly remove source from the destination node
        destinationNode = updateTree(destinationNode, [
            createRemoveUpdate(destinationNode, _.drop(sourcePath, destinationPath.length)),
        ]);
    } else {
        // Can remove source normally
        updates.push(createRemoveUpdate(root, sourcePath));

        // Have to drop in the correct destination after the source has been removed
        const removedNodeParentIsInPath = isPathPrefixEqual(sourcePath, destinationPath, sourcePath.length - 1);
        if (removedNodeParentIsInPath) {
            destinationPath.splice(sourcePath.length - 1, 1);
        }
    }

    const sourceNode = getAndAssertNodeAtPathExists(root, sourcePath);
    let first: MosaicNode<T>;
    let second: MosaicNode<T>;
    if (position === MosaicDropTargetPosition.LEFT || position === MosaicDropTargetPosition.TOP) {
        first = sourceNode;
        second = destinationNode;
    } else {
        first = destinationNode;
        second = sourceNode;
    }

    let direction: MosaicDirection = 'column';
    if (position === MosaicDropTargetPosition.LEFT || position === MosaicDropTargetPosition.RIGHT) {
        direction = 'row';
    }

    updates.push({
        path: destinationPath,
        spec: {
            $set: { first, second, direction },
        },
    });

    return updates;
}
/**
 * Sets the splitPercentage to hide the node at `path`
 * @param path
 * @returns {{path: T[], spec: {splitPercentage: {$set: number}}}}
 */
export function createHideUpdate<T>(path: MosaicPath): MosaicUpdate<T> {
    const targetPath = _.dropRight(path);
    const thisBranch = _.last(path);

    let splitPercentage: number;
    if (thisBranch === 'first') {
        splitPercentage = 0;
    } else {
        splitPercentage = 100;
    }

    return {
        path: targetPath,
        spec: {
            splitPercentage: {
                $set: splitPercentage,
            },
        },
    };
}

/**
 * Sets the splitPercentage of node at `path` and all of its parents to `percentage` in order to expand it
 * @param path
 * @param percentage
 * @returns {{spec: MosaicUpdateSpec<T>, path: Array}}
 */
export function createExpandUpdate<T>(path: MosaicPath, percentage: number): MosaicUpdate<T> {
    let spec: MosaicUpdateSpec<T> = {};
    for (let i = path.length - 1; i >= 0; i--) {
        const branch: MosaicBranch = path[i];
        const splitPercentage = branch === 'first' ? percentage : 100 - percentage;
        spec = {
            splitPercentage: {
                $set: splitPercentage,
            },
            [branch]: spec,
        };
    }

    return {
        spec,
        path: [],
    };
}