/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Rect} from './geometry';
import type {View} from './View';

export type LayoutInfo = {|view: View, frame: Rect|};
export type Layout = LayoutInfo[];

/**
 * A function that takes a list of subviews, currently laid out in
 * `existingLayout`, and lays them out into `containingFrame`.
 */
export type Layouter = (
  existingLayout: Layout,
  containingFrame: Rect,
) => Layout;

function viewToLayoutInfo(view: View): LayoutInfo {
  return {view, frame: view.frame};
}

export function viewsToLayout(views: View[]): Layout {
  return views.map(viewToLayoutInfo);
}

/**
 * Applies `layout`'s `frame`s to its corresponding `view`.
 */
export function collapseLayoutIntoViews(layout: Layout) {
  layout.forEach(({view, frame}) => view.setFrame(frame));
}

/**
 * A no-operation layout; does not modify the layout.
 */
export const noopLayout: Layouter = layout => layout;

/**
 * Layer views on top of each other. All views' frames will be set to
 * `containerFrame`.
 *
 * Equivalent to composing:
 * - `alignToContainerXLayout`,
 * - `alignToContainerYLayout`,
 * - `containerWidthLayout`, and
 * - `containerHeightLayout`.
 */
export const layeredLayout: Layouter = (layout, containerFrame) =>
  layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame}));

/**
 * Stacks `views` vertically in `frame`. All views in `views` will have their
 * widths set to the frame's width.
 */
export const verticallyStackedLayout: Layouter = (layout, containerFrame) => {
  let currentY = containerFrame.origin.y;
  return layout.map(layoutInfo => {
    const desiredSize = layoutInfo.view.desiredSize();
    const height = desiredSize
      ? desiredSize.height
      : containerFrame.origin.y + containerFrame.size.height - currentY;
    const proposedFrame = {
      origin: {x: containerFrame.origin.x, y: currentY},
      size: {width: containerFrame.size.width, height},
    };
    currentY += height;
    return {
      ...layoutInfo,
      frame: proposedFrame,
    };
  });
};

/**
 * A layouter that aligns all frames' lefts to the container frame's left.
 */
export const alignToContainerXLayout: Layouter = (layout, containerFrame) => {
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: {
        x: containerFrame.origin.x,
        y: layoutInfo.frame.origin.y,
      },
      size: layoutInfo.frame.size,
    },
  }));
};

/**
 * A layouter that aligns all frames' tops to the container frame's top.
 */
export const alignToContainerYLayout: Layouter = (layout, containerFrame) => {
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: {
        x: layoutInfo.frame.origin.x,
        y: containerFrame.origin.y,
      },
      size: layoutInfo.frame.size,
    },
  }));
};

/**
 * A layouter that sets all frames' widths to `containerFrame.size.width`.
 */
export const containerWidthLayout: Layouter = (layout, containerFrame) => {
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: layoutInfo.frame.origin,
      size: {
        width: containerFrame.size.width,
        height: layoutInfo.frame.size.height,
      },
    },
  }));
};

/**
 * A layouter that sets all frames' heights to `containerFrame.size.height`.
 */
export const containerHeightLayout: Layouter = (layout, containerFrame) => {
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: layoutInfo.frame.origin,
      size: {
        width: layoutInfo.frame.size.width,
        height: containerFrame.size.height,
      },
    },
  }));
};

/**
 * A layouter that sets all frames' heights to the desired height of its view.
 * If the view has no desired size, the frame's height is set to 0.
 */
export const desiredHeightLayout: Layouter = layout => {
  return layout.map(layoutInfo => {
    const desiredSize = layoutInfo.view.desiredSize();
    const height = desiredSize ? desiredSize.height : 0;
    return {
      ...layoutInfo,
      frame: {
        origin: layoutInfo.frame.origin,
        size: {
          width: layoutInfo.frame.size.width,
          height,
        },
      },
    };
  });
};

/**
 * A layouter that sets all frames' heights to the height of the tallest frame.
 */
export const uniformMaxSubviewHeightLayout: Layouter = layout => {
  const maxHeight = Math.max(
    ...layout.map(layoutInfo => layoutInfo.frame.size.height),
  );
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: layoutInfo.frame.origin,
      size: {
        width: layoutInfo.frame.size.width,
        height: maxHeight,
      },
    },
  }));
};

/**
 * A layouter that sets heights in this fashion:
 * - If a frame's height >= `containerFrame.size.height`, the frame is left unchanged.
 * - Otherwise, sets the frame's height to `containerFrame.size.height`.
 */
export const atLeastContainerHeightLayout: Layouter = (
  layout,
  containerFrame,
) => {
  return layout.map(layoutInfo => ({
    ...layoutInfo,
    frame: {
      origin: layoutInfo.frame.origin,
      size: {
        width: layoutInfo.frame.size.width,
        height: Math.max(
          containerFrame.size.height,
          layoutInfo.frame.size.height,
        ),
      },
    },
  }));
};

/**
 * Forces last view to take up the space below the second-last view.
 * Intended to be used with a vertical stack layout.
 */
export const lastViewTakesUpRemainingSpaceLayout: Layouter = (
  layout,
  containerFrame,
) => {
  if (layout.length === 0) {
    // Nothing to do
    return layout;
  }

  if (layout.length === 1) {
    // No second-last view; the view should just take up the container height
    return containerHeightLayout(layout, containerFrame);
  }

  const layoutInfoToPassThrough = layout.slice(0, layout.length - 1);
  const secondLastLayoutInfo =
    layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1];

  const remainingHeight =
    containerFrame.size.height -
    secondLastLayoutInfo.frame.origin.y -
    secondLastLayoutInfo.frame.size.height;
  const height = Math.max(remainingHeight, 0); // Prevent negative heights

  const lastLayoutInfo = layout[layout.length - 1];
  return [
    ...layoutInfoToPassThrough,
    {
      ...lastLayoutInfo,
      frame: {
        origin: lastLayoutInfo.frame.origin,
        size: {
          width: lastLayoutInfo.frame.size.width,
          height,
        },
      },
    },
  ];
};

/**
 * Create a layouter that applies each layouter in `layouters` in sequence.
 */
export function createComposedLayout(...layouters: Layouter[]): Layouter {
  if (layouters.length === 0) {
    return noopLayout;
  }

  const composedLayout: Layouter = (layout, containerFrame) => {
    return layouters.reduce(
      (intermediateLayout, layouter) =>
        layouter(intermediateLayout, containerFrame),
      layout,
    );
  };
  return composedLayout;
}
