// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.customizepublictransportstop;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.util.ArrayList;
import java.util.List;

import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.tools.Logging;

/**
 * Operation of creation and customizing stop area relation and its members
 * under user selection
 * 
 * @author Rodion Scherbakov
 */
public class CustomizeStopAreaOperation extends StopAreaOperationBase {

    /**
     * Operation name in undo list
     */
    private static final String TAG_ASSIGN_COMMAND_NAME = "Stop area tag assign";

    /**
     * Constructor of operation object
     * 
     * @param currentDataSet Current Josm data set
     */
    public CustomizeStopAreaOperation(DataSet currentDataSet) {
        super(currentDataSet);
    }

    /**
     * Forming commands for josm for saving name and name:en attributes stop of area
     * members and relation attributes
     * 
     * @param target Stop area member or relation
     * @param commands List of commands
     * @param stopArea Stop area object
     * @return Resulting list of commands
     */
    public List<Command> nameTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea) {
        if (commands == null)
            commands = new ArrayList<Command>();

        commands = assignTag(commands, target, OSMTags.NAME_TAG, "".equals(stopArea.name) ? null : stopArea.name);
        commands = assignTag(commands, target, OSMTags.NAME_EN_TAG,
                "".equals(stopArea.nameEn) ? null : stopArea.nameEn);
        return commands;
    }

    /**
     * Clear transport type tags
     * 
     * @param target Josm object for tag clearing
     * @param commands Command list
     * @return Resulting list of commands
     */
    protected List<Command> transportTypeTagClearing(OsmPrimitive target, List<Command> commands) {
        commands = clearTag(commands, target, OSMTags.BUS_TAG);
        commands = clearTag(commands, target, OSMTags.SHARE_TAXI_TAG);
        commands = clearTag(commands, target, OSMTags.TROLLEYBUS_TAG);
        commands = clearTag(commands, target, OSMTags.TRAM_TAG);
        commands = clearTag(commands, target, OSMTags.TRAIN_TAG);
        return commands;
    }

    /**
     * Assign transport type tags to node
     * 
     * @param target Josm object for tag assigning
     * @param commands Command list
     * @param stopArea Stop area object
     * @param isStopPoint Flag of stop point
     * @return Resulting list of commands
     */
    protected List<Command> transportTypeTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea,
            Boolean isStopPoint) {
        if (commands == null)
            commands = new ArrayList<Command>();

        if (isStopPoint) {
            if (stopArea.isTrainStop || stopArea.isTrainStation) {
                commands = clearTag(commands, target, OSMTags.BUS_TAG);
                commands = clearTag(commands, target, OSMTags.SHARE_TAXI_TAG);
                commands = clearTag(commands, target, OSMTags.TROLLEYBUS_TAG);
                commands = clearTag(commands, target, OSMTags.TRAM_TAG);
                commands = assignTag(commands, target, OSMTags.TRAIN_TAG, OSMTags.YES_TAG_VALUE);
            } else {
                commands = assignTag(commands, target, OSMTags.BUS_TAG, stopArea.isBus ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.SHARE_TAXI_TAG,
                        stopArea.isShareTaxi ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TROLLEYBUS_TAG,
                        stopArea.isTrolleybus ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TRAM_TAG,
                        stopArea.isTram ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TRAIN_TAG,
                        stopArea.isTrainStation || stopArea.isTrainStop ? OSMTags.YES_TAG_VALUE : null);
            }
        } else {
            if (stopArea.isAssignTransportType) {
                commands = assignTag(commands, target, OSMTags.BUS_TAG, stopArea.isBus ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.SHARE_TAXI_TAG,
                        stopArea.isShareTaxi ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TROLLEYBUS_TAG,
                        stopArea.isTrolleybus ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TRAM_TAG,
                        stopArea.isTram ? OSMTags.YES_TAG_VALUE : null);
                commands = assignTag(commands, target, OSMTags.TRAIN_TAG,
                        stopArea.isTrainStation || stopArea.isTrainStop ? OSMTags.YES_TAG_VALUE : null);
            } else {
                commands = transportTypeTagClearing(target, commands);
            }
        }
        return commands;
    }

    /**
     * Forming commands for josm for saving general attributes of stop area members
     * and relation
     * 
     * @param target Stop area member or relation
     * @param commands List of commands
     * @param stopArea Stop area object
     * @return Resulting list of commands
     */
    public List<Command> generalTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea) {
        if (commands == null)
            commands = new ArrayList<Command>();

        commands = nameTagAssign(target, commands, stopArea);
        commands = assignTag(commands, target, OSMTags.NETWORK_TAG,
                "".equals(stopArea.network) ? null : stopArea.network);
        commands = assignTag(commands, target, OSMTags.OPERATOR_TAG,
                "".equals(stopArea.operator) ? null : stopArea.operator);
        commands = assignTag(commands, target, OSMTags.SERVICE_TAG,
                null == stopArea.service || OSMTags.CITY_NETWORK_TAG_VALUE.equals(stopArea.service)
                        || OSMTags.CITY_NETWORK_TAG_VALUE_OLD.equals(stopArea.service) ? null
                        : stopArea.service);

        return commands;
    }

    /**
     * Forming commands for josm for saving stop position attributes
     * 
     * @param target Stop position node
     * @param commands Original command list
     * @param stopArea Stop area object
     * @param isFirst true, if target is first stop position in stop area
     * @return Resulting command list
     */
    public List<Command> stopPointTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea,
            Boolean isFirst) {
        if (commands == null)
            commands = new ArrayList<Command>();

        commands = generalTagAssign(target, commands, stopArea);
        commands = transportTypeTagAssign(target, commands, stopArea, true);
        if (isFirst) {
            if (stopArea.isTrainStop) {
                commands = assignTag(commands, target, OSMTags.RAILWAY_TAG, OSMTags.HALT_TAG_VALUE);
            } else if (stopArea.isTrainStation) {
                commands = assignTag(commands, target, OSMTags.RAILWAY_TAG, OSMTags.STATION_TAG_VALUE);
            } else if (stopArea.isTram)
                commands = assignTag(commands, target, OSMTags.RAILWAY_TAG, OSMTags.TRAM_STOP_TAG_VALUE);
            else
                commands = clearTag(commands, target, OSMTags.RAILWAY_TAG);
        } else {
            commands = clearTag(commands, target, OSMTags.RAILWAY_TAG);
        }
        if (compareTag(target, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE))
            commands = clearTag(commands, target, OSMTags.HIGHWAY_TAG);
        if (compareTag(target, OSMTags.AMENITY_TAG, OSMTags.BUS_STATION_TAG_VALUE))
            commands = clearTag(commands, target, OSMTags.AMENITY_TAG);
        commands = assignTag(commands, target, OSMTags.PUBLIC_TRANSPORT_TAG, OSMTags.STOP_POSITION_TAG_VALUE);
        return commands;
    }

    /**
     * Forming commands for josm for saving platform attributes
     * 
     * @param target Platform node or way
     * @param commands Original command list
     * @param stopArea Stop area object
     * @param isFirst true, if this platform is first in stop area
     * @return Resulting command list
     */
    public List<Command> platformTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea,
            Boolean isFirst) {
        if (commands == null)
            commands = new ArrayList<Command>();

        commands = generalTagAssign(target, commands, stopArea);
        commands = transportTypeTagAssign(target, commands, stopArea, false);

        if (compareTag(target, OSMTags.RAILWAY_TAG, OSMTags.HALT_TAG_VALUE)
                || compareTag(target, OSMTags.RAILWAY_TAG, OSMTags.STATION_TAG_VALUE))
            commands = clearTag(commands, target, OSMTags.RAILWAY_TAG);
        if (target instanceof Way && (stopArea.isTrainStop || stopArea.isTrainStation || stopArea.isTram))
            commands = assignTag(commands, target, OSMTags.RAILWAY_TAG, OSMTags.PLATFORM_TAG_VALUE);
        if (stopArea.isBus || stopArea.isShareTaxi || stopArea.isTrolleybus) {
            if (target instanceof Way)
                commands = assignTag(commands, target, OSMTags.HIGHWAY_TAG, OSMTags.PLATFORM_TAG_VALUE);
            else if (isFirst && !stopArea.isBusStation)
                commands = assignTag(commands, target, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE);
        }
        commands = assignTag(commands, target, OSMTags.PUBLIC_TRANSPORT_TAG, OSMTags.PLATFORM_TAG_VALUE);
        if (target == stopArea.selectedObject) {
            commands = assignTag(commands, target, OSMTags.BENCH_TAG, stopArea.isBench ? OSMTags.YES_TAG_VALUE : null);
            commands = assignTag(commands, target, OSMTags.SHELTER_TAG,
                    stopArea.isShelter ? OSMTags.YES_TAG_VALUE : null);
            commands = assignTag(commands, target, OSMTags.COVERED_TAG,
                    stopArea.isCovered ? OSMTags.YES_TAG_VALUE : null);
            commands = assignTag(commands, target, OSMTags.AREA_TAG, stopArea.isArea ? OSMTags.YES_TAG_VALUE : null);
        }
        return commands;
    }

    /**
     * Forming commands for josm for saving attributes of non stop position or
     * platform
     * 
     * @param target Member of stop area relation
     * @param commands Original command list
     * @param stopArea Stop area object
     * @return Resulting command list
     */
    public List<Command> otherMemberTagAssign(OsmPrimitive target, List<Command> commands, StopArea stopArea) {
        if (commands == null)
            commands = new ArrayList<Command>();

        commands = nameTagAssign(target, commands, stopArea);
        commands = clearTag(commands, target, OSMTags.NETWORK_TAG);
        commands = clearTag(commands, target, OSMTags.OPERATOR_TAG);
        commands = clearTag(commands, target, OSMTags.SERVICE_TAG);
        if (compareTag(target, OSMTags.RAILWAY_TAG, OSMTags.HALT_TAG_VALUE)
                || compareTag(target, OSMTags.RAILWAY_TAG, OSMTags.STATION_TAG_VALUE))
            commands = clearTag(commands, target, OSMTags.RAILWAY_TAG);
        return commands;
    }

    /**
     * Forming commands for josm for saving stop area relation attributes
     * 
     * @param commands Original command list
     * @param stopArea Stop area object
     * @return Resulting command list
     */
    private List<Command> createStopAreaRelation(List<Command> commands, StopArea stopArea) {
        if (commands == null)
            commands = new ArrayList<Command>();

        Relation newRelation = new Relation();
        for (Node node : stopArea.stopPoints) {
            newRelation.addMember(new RelationMember(OSMTags.STOP_ROLE, node));
        }
        for (OsmPrimitive platform : stopArea.platforms) {
            newRelation.addMember(new RelationMember(OSMTags.PLATFORM_ROLE, platform));
        }
        for (OsmPrimitive otherMember : stopArea.otherMembers) {
            newRelation.addMember(new RelationMember("", otherMember));
        }
        UndoRedoHandler.getInstance().add(new AddCommand(MainApplication.getLayerManager().getEditDataSet(), newRelation));
        commands = generalTagAssign(newRelation, commands, stopArea);
        commands = assignTag(commands, newRelation, OSMTags.TYPE_TAG, OSMTags.PUBLIC_TRANSPORT_TAG);
        commands = assignTag(commands, newRelation, OSMTags.PUBLIC_TRANSPORT_TAG, OSMTags.STOP_AREA_TAG_VALUE);
        return commands;
    }

    /**
     * Adding of new stop area members to relation
     * 
     * @param commands Original command list
     * @param targetRelation Stop area relation
     * @param member Stop area relation member
     * @param roleName Role name
     * @return Resulting command list
     */
    public static List<Command> addNewRelationMember(List<Command> commands, Relation targetRelation,
            OsmPrimitive member, String roleName) {
        if (commands == null)
            commands = new ArrayList<Command>();

        for (RelationMember relationMember : targetRelation.getMembers()) {
            if (relationMember.getMember() == member) {
                if (relationMember.getRole() == roleName) {
                    return commands;
                }
                return commands;
            }
        }
        targetRelation.addMember(new RelationMember(roleName, member));
        commands.add(new ChangeCommand(targetRelation, targetRelation));
        return commands;
    }

    /**
     * Adding new stop area members to relation
     * 
     * @param commands Original command list
     * @param stopArea Stop area object
     * @return Resulting command list
     */
    private List<Command> addNewRelationMembers(List<Command> commands, StopArea stopArea) {
        if (commands == null)
            commands = new ArrayList<Command>();

        for (OsmPrimitive stopPoint : stopArea.stopPoints) {
            commands = addNewRelationMember(commands, stopArea.stopAreaRelation, stopPoint, OSMTags.STOP_ROLE);
        }
        for (OsmPrimitive platform : stopArea.platforms) {
            commands = addNewRelationMember(commands, stopArea.stopAreaRelation, platform, OSMTags.PLATFORM_ROLE);
        }
        for (OsmPrimitive otherMember : stopArea.otherMembers) {
            commands = addNewRelationMember(commands, stopArea.stopAreaRelation, otherMember, null);
        }
        return commands;
    }

    /**
     * Testing, is josm object bus stop node or contains bus stop node, defined by
     * tag and its value
     * 
     * @param member Josm object
     * @param tag Tag name
     * @param tagValue Tag value
     * @return true, if josm object is bus stop node or contains bus stop node
     */
    private Node searchBusStop(OsmPrimitive member, String tag, String tagValue) {
        if (member instanceof Node) {
            if (compareTag(member, tag, tagValue)) {
                return (Node) member;
            }
        } else {
            Way memberWay = (Way) member;
            for (Node node : memberWay.getNodes()) {
                if (compareTag(node, tag, tagValue)) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * Testing, do stop area contains bus stop node, defined by tag and its value
     * 
     * @param tag Tag name
     * @param tagValue Tag value
     * @return true, if stop area contains bus stop node
     */
    public Node searchBusStop(StopArea stopArea, String tag, String tagValue) {
        for (OsmPrimitive platform : stopArea.platforms) {
            Node busStopNode = searchBusStop(platform, tag, tagValue);
            if (busStopNode != null)
                return busStopNode;
        }
        for (OsmPrimitive otherMember : stopArea.otherMembers) {
            Node busStopNode = searchBusStop(otherMember, tag, tagValue);
            if (busStopNode != null)
                return busStopNode;
        }
        return null;
    }

    /**
     * Testing, must stop area to have separate bus stop node
     * 
     * @param stopArea Stop area object
     * @param firstPlatform First platform of stop area JOSM object
     * @return True, stop area must to have separate bus stop node
     */
    public boolean needSeparateBusStop(StopArea stopArea, OsmPrimitive firstPlatform) {
        if (((stopArea.isBus || stopArea.isShareTaxi || stopArea.isTrolleybus) && (firstPlatform instanceof Way))) {
            return true;
        }
        return false;
    }

    /**
     * Clear excess tags from JOSM object and its nodes
     * 
     * @param commands Original command list
     * @param target JOSM object
     * @param tag Tag name
     * @param tagValue Tag value
     * @return Resulting command list
     */
    private List<Command> clearExcessTags(List<Command> commands, OsmPrimitive target, String tag, String tagValue) {
        if (commands == null)
            commands = new ArrayList<Command>();

        if (compareTag(target, tag, tagValue)) {
            commands = clearTag(commands, target, tag);
        }
        if (target instanceof Way) {
            Way memberWay = (Way) target;
            for (Node node : memberWay.getNodes()) {
                if (compareTag(node, tag, tagValue)) {
                    commands = clearTag(commands, target, tag);
                }
            }
        }
        return commands;
    }

    /**
     * Clear excess tags from JOSM object and its nodes
     * 
     * @param commands Command list
     * @param stopArea Stop area object
     * @param tag Tag name
     * @param tagValue Tag value
     * @return Resulting command list
     */
    public List<Command> clearExcessTags(List<Command> commands, StopArea stopArea, String tag, String tagValue) {
        if (commands == null)
            commands = new ArrayList<Command>();

        for (OsmPrimitive stopPoint : stopArea.stopPoints) {
            clearExcessTags(commands, stopPoint, tag, tagValue);
        }
        for (OsmPrimitive platform : stopArea.platforms) {
            clearExcessTags(commands, platform, tag, tagValue);
        }
        for (OsmPrimitive otherMember : stopArea.otherMembers) {
            clearExcessTags(commands, otherMember, tag, tagValue);
        }
        return commands;
    }

    /**
     * Create separate bus stop node or assign bus stop tag to platform node
     * 
     * @param commands Original command list
     * @param stopArea Stop area object
     * @param firstPlatform First platform in stop area relation
     * @param tag Tag name
     * @param tagValue Tag value
     * @return Resulting command list
     */
    protected List<Command> createSeparateBusStopNode(List<Command> commands, StopArea stopArea,
            OsmPrimitive firstPlatform, String tag, String tagValue) {
        if (commands == null)
            commands = new ArrayList<Command>();

        LatLon centerOfPlatform = getCenterOfWay(firstPlatform);
        if (firstPlatform instanceof Way) {
            if (centerOfPlatform != null) {
                Node newNode = new Node();
                newNode.setCoor(centerOfPlatform);
                UndoRedoHandler.getInstance().add(new AddCommand(MainApplication.getLayerManager().getEditDataSet(), newNode));
                UndoRedoHandler.getInstance().add(new ChangePropertyCommand(newNode, tag, tagValue));
                commands = assignTag(commands, newNode, tag, tagValue);
                stopArea.otherMembers.add(newNode);
            }
        } else {
            commands = assignTag(commands, firstPlatform, tag, tagValue);
        }
        return commands;
    }

    /**
     * Forming commands for JOSM for saving stop area members and relation
     * attributes
     * 
     * @param stopArea Stop area object
     * @return Resulting command list
     */
    public List<Command> customize(StopArea stopArea) {
        try {
            List<Command> commands = new ArrayList<Command>();
            Node separateBusStopNode = searchBusStop(stopArea, OSMTags.AMENITY_TAG, OSMTags.BUS_STATION_TAG_VALUE);
            if (separateBusStopNode == null)
                separateBusStopNode = searchBusStop(stopArea, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE);
            if (stopArea.isBusStation) {
                commands = clearExcessTags(commands, stopArea, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE);
            } else {
                commands = clearExcessTags(commands, stopArea, OSMTags.AMENITY_TAG, OSMTags.BUS_STATION_TAG_VALUE);
            }
            if (!(stopArea.isBus || stopArea.isShareTaxi || stopArea.isTrolleybus)) {
                commands = clearExcessTags(commands, stopArea, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE);
            }
            if (stopArea.stopPoints.size() == 0) {
                CreateNewStopPointOperation createNewStopPointOperation = new CreateNewStopPointOperation(
                        getCurrentDataSet());
                createNewStopPointOperation.performCustomizing(stopArea);
            }
            Boolean isFirst = true;
            for (Node node : stopArea.stopPoints) {
                commands = stopPointTagAssign(node, commands, stopArea, isFirst);
                isFirst = false;
            }
            isFirst = true;
            OsmPrimitive firstPlatform = null;
            for (OsmPrimitive platform : stopArea.platforms) {
                commands = platformTagAssign(platform, commands, stopArea, isFirst);
                if (isFirst)
                    firstPlatform = platform;
                isFirst = false;
            }
            if (needSeparateBusStop(stopArea, firstPlatform)) {
                if (stopArea.isBusStation) {
                    if (separateBusStopNode == null) {
                        commands = createSeparateBusStopNode(commands, stopArea, firstPlatform, OSMTags.AMENITY_TAG,
                                OSMTags.BUS_STATION_TAG_VALUE);
                    } else {
                        commands = assignTag(commands, separateBusStopNode, OSMTags.AMENITY_TAG,
                                OSMTags.BUS_STATION_TAG_VALUE);
                    }
                } else {
                    if (separateBusStopNode == null) {
                        commands = createSeparateBusStopNode(commands, stopArea, firstPlatform, OSMTags.HIGHWAY_TAG,
                                OSMTags.BUS_STOP_TAG_VALUE);
                    } else {
                        commands = assignTag(commands, separateBusStopNode, OSMTags.HIGHWAY_TAG,
                                OSMTags.BUS_STOP_TAG_VALUE);
                    }
                }
            } else {
                if (stopArea.isBusStation) {
                    commands = assignTag(commands, firstPlatform, OSMTags.AMENITY_TAG, OSMTags.BUS_STATION_TAG_VALUE);
                } else if (stopArea.isBus || stopArea.isTrolleybus || stopArea.isShareTaxi) {
                    commands = assignTag(commands, firstPlatform, OSMTags.HIGHWAY_TAG, OSMTags.BUS_STOP_TAG_VALUE);
                }
            }
            for (OsmPrimitive otherMember : stopArea.otherMembers) {
                commands = otherMemberTagAssign(otherMember, commands, stopArea);
            }
            if (stopArea.stopAreaRelation == null) {
                if (stopArea.stopPoints.size() + stopArea.platforms.size() + stopArea.otherMembers.size() > 1) {
                    commands = createStopAreaRelation(commands, stopArea);
                }
            } else {
                commands = generalTagAssign(stopArea.stopAreaRelation, commands, stopArea);
                commands = transportTypeTagClearing(stopArea.stopAreaRelation, commands);
                commands = addNewRelationMembers(commands, stopArea);
            }
            return commands;
        } catch (Exception ex) {
            MessageBox.ok(ex.getMessage());
        }
        return null;
    }

    /**
     * Construct and executing command list for customizing of stop area relation
     * and its members
     * 
     * @param stopArea Stop area object
     * @return Stop area object
     */
    @Override
    public StopArea performCustomizing(StopArea stopArea) {
        List<Command> commands = customize(stopArea);
        if (commands != null && !commands.isEmpty())
            try {
                UndoRedoHandler.getInstance().add(new SequenceCommand(tr(TAG_ASSIGN_COMMAND_NAME), commands));
                return stopArea;
            } catch (Exception ex) {
                Logging.error(ex);
            }
        return stopArea;
    }

}
