/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.support.avtransport.impl;

import org.teleal.cling.binding.LocalServiceBindingException;
import org.teleal.cling.model.ModelUtil;
import org.teleal.cling.model.types.ErrorCode;
import org.teleal.cling.model.types.UnsignedIntegerFourBytes;
import org.teleal.cling.support.avtransport.AVTransport;
import org.teleal.cling.support.avtransport.AVTransportErrorCode;
import org.teleal.cling.support.avtransport.AVTransportException;
import org.teleal.cling.support.avtransport.AbstractAVTransportService;
import org.teleal.cling.support.avtransport.dto.DeviceCapabilities;
import org.teleal.cling.support.avtransport.dto.MediaInfo;
import org.teleal.cling.support.avtransport.dto.PlayMode;
import org.teleal.cling.support.avtransport.dto.PositionInfo;
import org.teleal.cling.support.avtransport.dto.RecordQualityMode;
import org.teleal.cling.support.avtransport.dto.SeekMode;
import org.teleal.cling.support.avtransport.dto.TransportInfo;
import org.teleal.cling.support.avtransport.dto.TransportSettings;
import org.teleal.cling.support.avtransport.impl.state.AbstractState;
import org.teleal.common.statemachine.StateMachineBuilder;
import org.teleal.common.statemachine.TransitionException;

import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

/**
 *
 */
public abstract class AVTransportService<T extends AVTransport> extends AbstractAVTransportService {

    private static Logger log = Logger.getLogger(AVTransportService.class.getName());

    final private Map<Long, AVTransportStateMachine> instances = new ConcurrentHashMap();

    final Class<? extends AVTransportStateMachine> stateMachineDefinition;
    final Class<? extends AbstractState> initialState;
    final Class<? extends AVTransport> transportClass;

    public AVTransportService(Class<? extends AVTransportStateMachine> stateMachineDefinition,
                              Class<? extends AbstractState> initialState,
                              Class<T> transportClass) throws LocalServiceBindingException {
        this.stateMachineDefinition = stateMachineDefinition;
        this.initialState = initialState;
        this.transportClass = transportClass;
    }

    public void setAVTransportURI(UnsignedIntegerFourBytes instanceId,
                                  String currentURI,
                                  String currentURIMetaData) throws AVTransportException {

        try {
            new URI(currentURI);
        } catch (Exception ex) {
            throw new AVTransportException(
                    ErrorCode.INVALID_ARGS, "CurrentURI can not be null or malformed"
            );
        }

        try {
            AVTransportStateMachine transportStateMachine = findInstance(instanceId, true);
            transportStateMachine.setTransportURI(currentURI, currentURIMetaData);
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void setNextAVTransportURI(UnsignedIntegerFourBytes instanceId,
                                      String nextURI,
                                      String nextURIMetaData) throws AVTransportException {

        try {
            new URI(nextURI);
        } catch (Exception ex) {
            throw new AVTransportException(
                    ErrorCode.INVALID_ARGS, "NextURI can not be null or malformed"
            );
        }

        try {
            AVTransportStateMachine transportStateMachine = findInstance(instanceId, true);
            transportStateMachine.setNextTransportURI(nextURI, nextURIMetaData);
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void setPlayMode(UnsignedIntegerFourBytes instanceId, String newPlayMode) throws AVTransportException {
        AVTransport transport = findInstance(instanceId).getCurrentState().getTransport();
        try {
            transport.setTransportSettings(
                    new TransportSettings(
                            PlayMode.valueOf(newPlayMode),
                            transport.getTransportSettings().getRecQualityMode()
                    )
            );
        } catch (IllegalArgumentException ex) {
            throw new AVTransportException(
                    AVTransportErrorCode.PLAYMODE_NOT_SUPPORTED, "Unsupported play mode: " + newPlayMode
            );
        }
    }

    public void setRecordQualityMode(UnsignedIntegerFourBytes instanceId, String newRecordQualityMode) throws AVTransportException {
        AVTransport transport = findInstance(instanceId).getCurrentState().getTransport();
        try {
            transport.setTransportSettings(
                    new TransportSettings(
                            transport.getTransportSettings().getPlayMode(),
                            RecordQualityMode.getByProtocolString(newRecordQualityMode)
                    )
            );
        } catch (IllegalArgumentException ex) {
            throw new AVTransportException(
                    AVTransportErrorCode.RECORDQUALITYMODE_NOT_SUPPORTED, "Unsupported record quality mode: " + newRecordQualityMode
            );
        }
    }

    public MediaInfo getMediaInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId).getCurrentState().getTransport().getMediaInfo();
    }

    public TransportInfo getTransportInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId).getCurrentState().getTransport().getTransportInfo();
    }

    public PositionInfo getPositionInfo(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId).getCurrentState().getTransport().getPositionInfo();
    }

    public DeviceCapabilities getDeviceCapabilities(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId).getCurrentState().getTransport().getDeviceCapabilities();
    }

    public TransportSettings getTransportSettings(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId).getCurrentState().getTransport().getTransportSettings();
    }

    public String getCurrentTransportActions(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        AVTransportStateMachine stateMachine = findInstance(instanceId);
        try {
            String[] actions = stateMachine.getCurrentState().getCurrentTransportActions();
            return ModelUtil.toCommaSeparatedList(actions);
        } catch (TransitionException ex) {
            return ""; // TODO: Empty string is not defined in spec but seems reasonable for no available action?
        }
    }

    public void stop(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            findInstance(instanceId).stop();
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void play(UnsignedIntegerFourBytes instanceId, String speed) throws AVTransportException {
        try {
            findInstance(instanceId).play(speed);
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void pause(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            findInstance(instanceId).pause();
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void record(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            findInstance(instanceId).record();
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void seek(UnsignedIntegerFourBytes instanceId, String unit, String target) throws AVTransportException {
        SeekMode seekMode;
        try {
             seekMode = SeekMode.getByProtocolString(unit);
        } catch (IllegalArgumentException ex) {
            throw new AVTransportException(
                    AVTransportErrorCode.SEEKMODE_NOT_SUPPORTED, "Unsupported seek mode: " + unit
            );
        }

        try {
            findInstance(instanceId).seek(seekMode, target);
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void next(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            findInstance(instanceId).next();
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    public void previous(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        try {
            findInstance(instanceId).previous();
        } catch (TransitionException ex) {
            throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage());
        }
    }

    protected AVTransportStateMachine findInstance(UnsignedIntegerFourBytes instanceId) throws AVTransportException {
        return findInstance(instanceId, true);
    }

    protected AVTransportStateMachine findInstance(UnsignedIntegerFourBytes instanceId, boolean createDefaultTransport) throws AVTransportException {
        synchronized (this) {
            long id = instanceId.getValue();
            AVTransportStateMachine instance = instances.get(id);
            if (instance == null && id == 0 && createDefaultTransport) {
                log.fine("Creating default transport instance with ID '0'");
                instance = createTransportStateMachine(id);
                instances.put(id, instance);
            } else if (instance == null) {
                throw new AVTransportException(AVTransportErrorCode.INVALID_INSTANCE_ID);
            }
            log.fine("Found transport control with ID '" + id + "'");
            return instance;
        }
    }

    protected AVTransportStateMachine createTransportStateMachine(long id) {
        // Create a proxy that delegates all calls to the right state implementation, working on the T state
        return StateMachineBuilder.build(
                stateMachineDefinition,
                initialState,
                new Class[]{transportClass},
                new Object[]{createTransport(id)}
        );
    }

    protected abstract T createTransport(long id);


}
