/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.yamahareceiver.internal.protocol.xml;

import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.GET_PARAM;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;

import java.io.IOException;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;

/**
 * This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
 * other inputs are using the same way of playback control.
 *
 * The XML nodes <Play_Info> and <Play_Control> are used.
 *
 * Example:
 *
 * InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
 * menu.goToPath(menuDir);
 * menu.selectItem(stationName);
 *
 * No state will be saved in here, but in {@link PlayInfoState} and
 * {@link PresetInfoState} instead.
 *
 * @author David Graeff
 * @author Tomasz Maruszak - Compatibility fixes
 */
public class InputWithPresetControlXML extends AbstractInputControlXML implements InputWithPresetControl {

    protected CommandTemplate preset = new CommandTemplate(
            "<Play_Control><Preset><Preset_Sel>%s</Preset_Sel></Preset></Play_Control>",
            "Play_Control/Preset/Preset_Sel");

    private final PresetInfoStateListener observer;

    /**
     * Create a InputWithPlayControl object for altering menu positions and requesting current menu information as well
     * as controlling the playback and choosing a preset item.
     *
     * @param inputID The input ID like USB or NET_RADIO.
     * @param con The Yamaha communication object to send http requests.
     */
    public InputWithPresetControlXML(String inputID, AbstractConnection con, PresetInfoStateListener observer,
            DeviceInformationState deviceInformationState) {
        super(LoggerFactory.getLogger(InputWithPresetControlXML.class), inputID, con, deviceInformationState);

        this.observer = observer;

        this.applyModelVariations();
    }

    /**
     * Apply command changes to ensure compatibility with all supported models
     */
    protected void applyModelVariations() {
        if (deviceDescriptor == null) {
            logger.trace("Descriptor not available");
            return;
        }

        // add compatibility adjustments here (if any)
    }

    /**
     * Updates the preset information
     *
     * @throws Exception
     */
    @Override
    public void update() throws IOException, ReceivedMessageParseException {
        if (observer == null) {
            return;
        }

        AbstractConnection con = comReference.get();
        Node response = getResponse(con,
                wrInput("<Play_Control><Preset><Preset_Sel_Item>GetParam</Preset_Sel_Item></Preset></Play_Control>"),
                inputElement);

        PresetInfoState msg = new PresetInfoState();

        // Set preset channel names, obtained from this xpath:
        // NET_RADIO/Play_Control/Preset/Preset_Sel_Item/Item_1/Title
        Node presetNode = getNode(response, "Play_Control/Preset/Preset_Sel_Item");
        if (presetNode != null) {
            for (int i = 1; i <= PRESET_CHANNELS; i++) {
                Node itemNode = getNode(presetNode, "Item_" + i);
                if (itemNode == null) {
                    break;
                }

                String title = getNodeContentOrDefault(itemNode, "Title", "Item_" + i);
                String value = getNodeContentOrDefault(itemNode, "Param", String.valueOf(i));

                // For RX-V3900 when a preset slot is not used, this is how it looks
                if (StringUtils.isEmpty(title) && "Not Used".equalsIgnoreCase(value)) {
                    continue;
                }

                int presetChannel = convertToPresetNumber(value);
                PresetInfoState.Preset preset = new PresetInfoState.Preset(title, presetChannel);
                msg.presetChannelNames.add(preset);
            }
        }
        msg.presetChannelNamesChanged = true;

        String presetValue = getNodeContentOrEmpty(response, preset.getPath());

        // fall back to second method of obtaining current preset (works for Tuner on RX-V3900)
        if (StringUtils.isEmpty(presetValue)) {
            try {
                Node presetResponse = getResponse(con, wrInput(preset.apply(GET_PARAM)), inputElement);
                presetValue = getNodeContentOrEmpty(presetResponse, preset.getPath());
            } catch (IOException | ReceivedMessageParseException e) {
                // this is on purpose, in case the AVR does not support this request and responds with error or nonsense
            }
        }

        // For Tuner input on RX-V3900 this is not a number (e.g. "A1" or "B1").
        msg.presetChannel = convertToPresetNumber(presetValue);

        observer.presetInfoUpdated(msg);
    }

    private int convertToPresetNumber(String presetValue) {
        if (StringUtils.isNotEmpty(presetValue)) {
            if (StringUtils.isNumeric(presetValue)) {
                return Integer.parseInt(presetValue);
            } else {
                // special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
                if (presetValue.length() >= 2) {
                    Character presetAlpha = presetValue.charAt(0);
                    if (Character.isLetter(presetAlpha) && Character.isUpperCase(presetAlpha)) {
                        int presetNumber = Integer.parseInt(presetValue.substring(1));
                        return (ArrayUtils.indexOf(LETTERS, presetAlpha) + 1) * 100 + presetNumber;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Select a preset channel.
     *
     * @param presetChannel The preset position [1,40]
     * @throws Exception
     */
    @Override
    public void selectItemByPresetNumber(int presetChannel) throws IOException, ReceivedMessageParseException {
        String presetValue;

        // special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
        if (presetChannel > 100) {
            int presetNumber = presetChannel % 100;
            char presetAlpha = LETTERS[presetChannel / 100 - 1];
            presetValue = Character.toString(presetAlpha) + presetNumber;
        } else {
            presetValue = Integer.toString(presetChannel);
        }

        String cmd = wrInput(preset.apply(presetValue));
        comReference.get().send(cmd);
        update();
    }

    private static final Character[] LETTERS = new Character[] { 'A', 'B', 'C', 'D' };
}
