package de.metas.ui.web.picking.pickingslot;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.adempiere.warehouse.LocatorId;
import org.adempiere.warehouse.WarehouseId;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

import de.metas.handlingunits.HuId;
import de.metas.i18n.ITranslatableString;
import de.metas.i18n.TranslatableStrings;
import de.metas.picking.api.PickingSlotId;
import de.metas.product.ProductId;
import de.metas.ui.web.handlingunits.HUEditorRowType;
import de.metas.ui.web.handlingunits.WEBUI_HU_Constants;
import de.metas.ui.web.picking.PickingConstants;
import de.metas.ui.web.view.IViewRow;
import de.metas.ui.web.view.ViewId;
import de.metas.ui.web.view.ViewRowFieldNameAndJsonValues;
import de.metas.ui.web.view.ViewRowFieldNameAndJsonValuesHolder;
import de.metas.ui.web.view.descriptor.annotation.ViewColumn;
import de.metas.ui.web.view.descriptor.annotation.ViewColumn.ViewColumnLayout;
import de.metas.ui.web.view.json.JSONViewDataType;
import de.metas.ui.web.window.datatypes.DocumentId;
import de.metas.ui.web.window.datatypes.DocumentPath;
import de.metas.ui.web.window.datatypes.LookupValue;
import de.metas.ui.web.window.datatypes.MediaType;
import de.metas.ui.web.window.datatypes.json.JSONLookupValue;
import de.metas.ui.web.window.descriptor.DocumentFieldWidgetType;
import lombok.Builder;
import lombok.NonNull;
import lombok.ToString;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2017 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

/**
 * Rows shown in {@link PickingSlotView}. One row can represent a picking slot, a TU or a picked good (CU).
 *
 * @author metas-dev <[email protected]>
 *
 */
@ToString
public final class PickingSlotRow implements IViewRow
{
	public static PickingSlotRow cast(final IViewRow row)
	{
		return (PickingSlotRow)row;
	}

	private final PickingSlotRowId pickingSlotRowId;
	private final PickingSlotRowType type;
	private final DocumentPath documentPath;
	private final boolean processed;
	private final ImmutableMap<PickingSlotRowId, PickingSlotRow> includedHURows;
	private final ViewRowFieldNameAndJsonValuesHolder<PickingSlotRow> values = ViewRowFieldNameAndJsonValuesHolder.newInstance(PickingSlotRow.class);

	private final ViewId includedViewId;

	//
	// Picking slot
	private final LookupValue pickingSlotBPartner;
	private final LookupValue pickingSlotBPLocation;
	private final LookupValue pickingSlotWarehouse;
	private final LocatorId pickingSlotLocatorId;
	private final ITranslatableString pickingSlotCaption;

	//
	// HU
	private final boolean huTopLevel;

	@ViewColumn(captionKey = "HUCode", widgetType = DocumentFieldWidgetType.Text, layouts = {
			@ViewColumnLayout(when = JSONViewDataType.grid, seqNo = 10),
			@ViewColumnLayout(when = JSONViewDataType.includedView, seqNo = 10)
	})
	private final String huCode;

	@ViewColumn(captionKey = "M_Product_ID", widgetType = DocumentFieldWidgetType.Lookup, layouts = {
			@ViewColumnLayout(when = JSONViewDataType.grid, seqNo = 30),
			@ViewColumnLayout(when = JSONViewDataType.includedView, seqNo = 30)
	})
	private final JSONLookupValue huProduct;

	@ViewColumn(captionKey = "M_HU_PI_Item_Product_ID", widgetType = DocumentFieldWidgetType.Text, //
			restrictToMediaTypes = { MediaType.SCREEN }, //
			layouts = {
					@ViewColumnLayout(when = JSONViewDataType.grid, seqNo = 40),
					@ViewColumnLayout(when = JSONViewDataType.includedView, seqNo = 40)
			})
	private final String huPackingInfo;

	@ViewColumn(captionKey = "QtyCU", widgetType = DocumentFieldWidgetType.Quantity, layouts = {
			@ViewColumnLayout(when = JSONViewDataType.grid, seqNo = 50),
			@ViewColumnLayout(when = JSONViewDataType.includedView, seqNo = 50)
	})
	private final BigDecimal huQtyCU;

	@Builder(builderMethodName = "fromSourceHUBuilder", builderClassName = "FromSourceHUBuilder")
	private PickingSlotRow(
			final HuId huId,
			final HUEditorRowType huEditorRowType,
			final String huCode,
			final JSONLookupValue product,
			final String packingInfo,
			final BigDecimal qtyCU,
			final boolean topLevelHU)
	{
		pickingSlotRowId = PickingSlotRowId.ofSourceHU(huId);

		this.type = PickingSlotRowType.forPickingHuRow(huEditorRowType);
		processed = true;
		documentPath = DocumentPath.rootDocumentPath(WEBUI_HU_Constants.WEBUI_HU_Window_ID, huId);

		//
		// HU
		this.huCode = huCode;
		huProduct = product;
		huPackingInfo = packingInfo;
		huQtyCU = qtyCU;
		huTopLevel = topLevelHU;

		//
		// Picking slot info
		pickingSlotWarehouse = null;
		pickingSlotLocatorId = null;
		pickingSlotBPartner = null;
		pickingSlotBPLocation = null;
		pickingSlotCaption = null;

		//
		// Included rows - we don't show any
		this.includedHURows = ImmutableMap.of();

		this.includedViewId = null;
	}

	/**
	 * Picking slot row constructor.
	 *
	 * @param pickingSlotId
	 * @param pickingSlotName
	 * @param pickingSlotWarehouse
	 * @param pickingSlotBPartner
	 * @param pickingSlotBPLocation
	 * @param includedHURows
	 */
	@Builder(builderMethodName = "fromPickingSlotBuilder", builderClassName = "FromPickingSlotBuilder")
	private PickingSlotRow(
			final PickingSlotId pickingSlotId,
			//
			final String pickingSlotName,
			final LookupValue pickingSlotWarehouse,
			final LocatorId pickingSlotLocatorId,
			final LookupValue pickingSlotBPartner,
			final LookupValue pickingSlotBPLocation,
			//
			final List<PickingSlotRow> includedHURows,
			final ViewId includedViewId)
	{
		pickingSlotRowId = PickingSlotRowId.ofPickingSlotId(pickingSlotId);

		type = PickingSlotRowType.forPickingSlotRow();
		processed = false;
		documentPath = DocumentPath.rootDocumentPath(PickingConstants.WINDOWID_PickingSlotView, pickingSlotId);

		// HU
		huCode = null;
		huProduct = null;
		huPackingInfo = null;
		huQtyCU = null;
		huTopLevel = false;

		// Picking slot info
		this.pickingSlotWarehouse = pickingSlotWarehouse;
		this.pickingSlotLocatorId = pickingSlotLocatorId;
		this.pickingSlotBPartner = pickingSlotBPartner;
		this.pickingSlotBPLocation = pickingSlotBPLocation;

		pickingSlotCaption = buildPickingSlotCaption(pickingSlotName, pickingSlotBPartner, pickingSlotBPLocation);

		// Included rows
		this.includedHURows = includedHURows == null ? ImmutableMap.of() : Maps.uniqueIndex(includedHURows, PickingSlotRow::getPickingSlotRowId);

		this.includedViewId = includedViewId;
	}

	/**
	 * Constructor for a row that represents an HU which is already assigned to a picking slot (aka a picked HU).
	 *
	 * @param pickingSlotId
	 * @param huId
	 * @param huStorageProductId
	 * @param type
	 * @param processed
	 * @param code
	 * @param product
	 * @param packingInfo
	 * @param qtyCU
	 * @param includedHURows
	 */
	@Builder(builderMethodName = "fromPickedHUBuilder", builderClassName = "FromPickedHUBuilder")
	private PickingSlotRow(
			@NonNull final PickingSlotId pickingSlotId,
			@NonNull final HuId huId,
			final int huStorageProductId,
			//
			final HUEditorRowType huEditorRowType,
			final boolean processed,
			//
			final String huCode,
			final JSONLookupValue product,
			final String packingInfo,
			final BigDecimal qtyCU,
			final boolean topLevelHU,
			//
			final List<PickingSlotRow> includedHURows)
	{
		pickingSlotRowId = PickingSlotRowId.ofPickedHU(pickingSlotId, huId, huStorageProductId);

		this.type = PickingSlotRowType.forPickingHuRow(huEditorRowType);
		this.processed = processed;

		documentPath = DocumentPath.rootDocumentPath(WEBUI_HU_Constants.WEBUI_HU_Window_ID, huId);

		// HU
		this.huCode = huCode;
		huProduct = product;
		huPackingInfo = packingInfo;
		huQtyCU = qtyCU;
		huTopLevel = topLevelHU;

		// Picking slot info
		pickingSlotWarehouse = null;
		pickingSlotLocatorId = null;
		pickingSlotBPartner = null;
		pickingSlotBPLocation = null;
		pickingSlotCaption = null;

		// Included rows
		this.includedHURows = includedHURows == null ? ImmutableMap.of() : Maps.uniqueIndex(includedHURows, PickingSlotRow::getPickingSlotRowId);

		this.includedViewId = null;
	}

	private static ITranslatableString buildPickingSlotCaption(final String pickingSlotName, final LookupValue pickingSlotBPartner, final LookupValue pickingSlotBPLocation)
	{
		return TranslatableStrings.join(" ",
				TranslatableStrings.constant(pickingSlotName),
				pickingSlotBPartner != null ? pickingSlotBPartner.getDisplayNameTrl() : TranslatableStrings.empty(),
				pickingSlotBPLocation != null ? pickingSlotBPLocation.getDisplayNameTrl() : TranslatableStrings.empty());
	}

	@Override
	public DocumentId getId()
	{
		return pickingSlotRowId.toDocumentId();
	}

	public PickingSlotRowId getPickingSlotRowId()
	{
		return pickingSlotRowId;
	}

	@Override
	public PickingSlotRowType getType()
	{
		return type;
	}

	@Override
	public boolean isProcessed()
	{
		return processed;
	}

	@Override
	public DocumentPath getDocumentPath()
	{
		return documentPath;
	}

	@Override
	public ImmutableSet<String> getFieldNames()
	{
		return values.getFieldNames();
	}

	@Override
	public ViewRowFieldNameAndJsonValues getFieldNameAndJsonValues()
	{
		return values.get(this);
	}

	@Override
	public List<PickingSlotRow> getIncludedRows()
	{
		return ImmutableList.copyOf(includedHURows.values());
	}

	public Stream<PickingSlotRow> streamIncludedRowsRecursivelly()
	{
		return includedHURows.values()
				.stream()
				.flatMap(PickingSlotRow::streamThisRowAndIncludedRowsRecursivelly);
	}

	public Stream<PickingSlotRow> streamThisRowAndIncludedRowsRecursivelly()
	{
		final Stream<PickingSlotRow> thisRowStream = Stream.of(this);
		final Stream<PickingSlotRow> includedRowsStream = streamIncludedRowsRecursivelly();
		return Stream.concat(thisRowStream, includedRowsStream);
	}

	public Optional<PickingSlotRow> findIncludedRowById(final PickingSlotRowId includedRowId)
	{
		// This
		if (this.pickingSlotRowId.equals(includedRowId))
		{
			return Optional.of(this);
		}

		// Direct children
		{
			final PickingSlotRow row = includedHURows.get(includedRowId);
			if (row != null)
			{
				return Optional.of(row);
			}
		}

		// Ask all included HU rows
		return includedHURows.values()
				.stream()
				.map(includedHURow -> includedHURow.findIncludedRowById(includedRowId).orElse(null))
				.filter(row -> row != null)
				.findFirst();
	}

	@Override
	public boolean isSingleColumn()
	{
		return isPickingSlotRow();
	}

	@Override
	public ITranslatableString getSingleColumnCaption()
	{
		return pickingSlotCaption;
	}

	/**
	 * @return {@code true} if this row represents an actual picking slot and not any sort of HU that is also shown in this view.
	 */
	public boolean isPickingSlotRow()
	{
		return pickingSlotRowId.isPickingSlotRow();
	}

	public PickingSlotId getPickingSlotId()
	{
		return getPickingSlotRowId().getPickingSlotId();
	}

	public HuId getHuId()
	{
		return pickingSlotRowId.getHuId();
	}

	/**
	 * @return {@code true} is this row represents an HU that is assigned to a picking slot
	 */
	public boolean isPickedHURow()
	{
		return pickingSlotRowId.isPickedHURow();
	}

	public boolean isTopLevelHU()
	{
		return huTopLevel;
	}

	public boolean isLU()
	{
		return isPickedHURow() && getType().isLU();
	}

	public boolean isTU()
	{
		return isPickedHURow() && getType().isTU();
	}

	public boolean isCU()
	{
		return isPickedHURow() && getType().isCU();
	}

	public boolean isCUOrStorage()
	{
		return isPickedHURow() && getType().isCUOrStorage();
	}

	/**
	 *
	 * @return {@code true} if this row represents an HU that is a source-HU for fine-picking.
	 */
	public boolean isPickingSourceHURow()
	{
		return pickingSlotRowId.isPickingSourceHURow();
	}

	public WarehouseId getPickingSlotWarehouseId()
	{
		return pickingSlotWarehouse != null ? WarehouseId.ofRepoId(pickingSlotWarehouse.getIdAsInt()) : null;
	}

	public BigDecimal getHuQtyCU()
	{
		return huQtyCU;
	}

	public ProductId getHuProductId()
	{
		return huProduct != null ? ProductId.ofRepoId(huProduct.getKeyAsInt()) : null;
	}

	@Override
	public ViewId getIncludedViewId()
	{
		return includedViewId;
	}

	public LocatorId getPickingSlotLocatorId()
	{
		return pickingSlotLocatorId;
	}

	public int getBPartnerId()
	{
		return pickingSlotBPartner != null ? pickingSlotBPartner.getIdAsInt() : -1;
	}

	public int getBPartnerLocationId()
	{
		return pickingSlotBPLocation != null ? pickingSlotBPLocation.getIdAsInt() : -1;
	}
}