/*
 * 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.model.meta;

import org.teleal.cling.model.Constants;
import org.teleal.cling.model.Resource;
import org.teleal.cling.model.Validatable;
import org.teleal.cling.model.ValidationError;
import org.teleal.cling.model.ValidationException;
import org.teleal.cling.model.types.DeviceType;
import org.teleal.cling.model.types.ServiceId;
import org.teleal.cling.model.types.ServiceType;
import org.teleal.cling.model.types.UDN;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;


public abstract class Device<DI extends DeviceIdentity, D extends Device, S extends Service> implements Validatable {

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

    final private Phase phase;

    final private DI identity;

    final private UDAVersion version;
    final private DeviceType type;
    final private DeviceDetails details;
    final private Icon[] icons;
    final private DeviceService<S>[] deviceServices;
    final private Resource[] resources;

    final private D[] embeddedDevices;

    // Package mutable state
    private D parentDevice;

    public Device(DI identity) throws ValidationException {
        this(identity, null, null, null, null, null, null);
    }

    public Device(DI identity, DeviceType type, DeviceDetails details,
                  Icon[] icons, DeviceService<S>[] deviceServices) throws ValidationException {
        this(identity, null, type, details, icons, deviceServices, null);
    }

    public Device(DI identity, DeviceType type, DeviceDetails details,
                  Icon[] icons, DeviceService<S>[] deviceServices, D[] embeddedDevices) throws ValidationException {
        this(identity, null, type, details, icons, deviceServices, embeddedDevices);
    }

    public Device(DI identity, UDAVersion version, DeviceType type, DeviceDetails details,
                  Icon[] icons, DeviceService<S>[] deviceServices, D[] embeddedDevices) throws ValidationException {

        // State is:
        // - discovered if we have nothing but the identifier
        // - described if one of the services is not hydrated
        // - hydrated if all of the services are hydrated
        if (type != null) {
            boolean isHydrated = true;
            for (DeviceService deviceService : deviceServices) {
                if (!deviceService.getState().equals(Phase.HYDRATED)) {
                    isHydrated = false;
                    break;
                }
            }
            if (isHydrated) {
                this.phase = Phase.HYDRATED;
            } else {
                this.phase = Phase.DESCRIBED;
            }
        } else {
            this.phase = Phase.DISCOVERED;
        }

        this.identity = identity;
        this.version = version == null ? new UDAVersion() : version;
        this.type = type;
        this.details = details;

        this.icons = icons == null ? new Icon[0] : icons;
        for (Icon icon : this.icons) {
            icon.setDevice(this);
        }

        this.deviceServices = deviceServices == null ? new DeviceService[0] : deviceServices;
        for (DeviceService deviceService : this.deviceServices) {
            deviceService.setDevice(this);
        }

        this.embeddedDevices = embeddedDevices;
        if (embeddedDevices != null) {
            for (D embeddedDevice : embeddedDevices) {
                embeddedDevice.setParentDevice(this);
            }
        }

        List<ValidationError> errors = validate();

        if (isRoot()) {
            log.fine("Discovering local resources of device graph");
            Resource[] discoveredResources = discoverResources();
            Set<Resource> resources = new HashSet();
            for (Resource resource : discoveredResources) {
                log.finer("Discovered: " + resource);
                if (!resources.add(resource)) {
                    log.finer("Local resource already exists, queueing validation error");
                    errors.add(new ValidationError(
                            getClass(),
                            "resources",
                            "Local URI namespace conflict between resources of device: " + resource
                    ));
                }
            }
            this.resources = resources.toArray(new Resource[resources.size()]);
        } else {
            this.resources = null;
        }

        if (errors.size() > 0) {
            throw new ValidationException("Validation of device graph failed, call getErrors() on exception", errors);
        }
    }

    public Phase getPhase() {
        return phase;
    }

    public DI getIdentity() {
        return identity;
    }

    public UDAVersion getVersion() {
        return version;
    }

    public DeviceType getType() {
        return type;
    }

    public DeviceDetails getDetails() {
        return details;
    }

    public Icon[] getIcons() {
        return icons;
    }

    public boolean hasIcons() {
        return getIcons() != null && getIcons().length > 0;
    }

    public DeviceService<S>[] getDeviceServices() {
        return deviceServices;
    }

    public boolean hasDeviceServices() {
        return getDeviceServices() != null && getDeviceServices().length > 0;
    }

    public D[] getEmbeddedDevices() {
        return embeddedDevices;
    }

    public boolean hasEmbeddedDevices() {
        return getEmbeddedDevices() != null && getEmbeddedDevices().length > 0;
    }

    public D getParentDevice() {
        return parentDevice;
    }

    void setParentDevice(D parentDevice) {
        if (this.parentDevice != null)
            throw new IllegalStateException("Final value has been set already, model is immutable");
        this.parentDevice = parentDevice;
    }

    public Resource[] getResources() {
        return resources;
    }

    public boolean isRoot() {
        return getParentDevice() == null;
    }

    public Resource getLocalResource(URI localPathQuery) {
        for (Resource localResource : getResources()) {
            if (localResource.matchesPathQuery(localPathQuery))
                return localResource;
        }
        return null;
    }

    public ServiceType[] getServiceTypes() {
        Set<ServiceType> serviceTypes = new HashSet();
        for (DeviceService DeviceService : getDeviceServices()) {
            serviceTypes.add(DeviceService.getServiceType());
        }
        if (hasEmbeddedDevices()) {
            for (Device embeddedDevice : getEmbeddedDevices()) {
                serviceTypes.addAll(Arrays.asList(embeddedDevice.getServiceTypes()));
            }
        }
        return serviceTypes.toArray(new ServiceType[serviceTypes.size()]);
    }

    public List<DeviceService<S>> getDeviceServices(ServiceType serviceType) {
        List<DeviceService<S>> matches = new ArrayList();
        for (DeviceService ds : getDeviceServices()) {
            if (ds.getServiceType().equals(serviceType)) {
                matches.add(ds);
            }
        }
        return matches;
    }

    public DeviceService<S> getFirstDeviceService(ServiceType serviceType) {
        List<DeviceService<S>> services = getDeviceServices(serviceType);
        if (services.size() > 0) {
            return services.get(0);
        }
        return null;
    }

    public DeviceService<S> getDeviceService(ServiceId serviceId) {
        for (DeviceService  ds : getDeviceServices()) {
            if (ds.getServiceId().equals(serviceId))
                return ds;
        }
        return null;
    }

    public String getDisplayString() {

        // The goal is to have a clean string with "<manufacturer> <model name> <model#>"

        String cleanModelName = null;
        String cleanModelNumber = null;

        if (getDetails() != null && getDetails().getModelDetails() != null) {

            // Some vendors end the model name with the model number, let's remove that
            ModelDetails modelDetails = getDetails().getModelDetails();
            if (modelDetails.getModelName() != null) {
                cleanModelName = modelDetails.getModelName().endsWith(modelDetails.getModelNumber())
                        ? modelDetails.getModelName().substring(0, modelDetails.getModelName().length() - modelDetails.getModelNumber().length())
                        : modelDetails.getModelName();
            }

            // Some vendors repeat the model name as the model number, no good
            if (cleanModelName != null) {
                cleanModelNumber = cleanModelName.startsWith(modelDetails.getModelNumber())
                        ? ""
                        : modelDetails.getModelNumber();
            } else {
                cleanModelNumber = modelDetails.getModelNumber();
            }
        }

        StringBuilder sb = new StringBuilder();

        if (getDetails() != null && getDetails().getManufacturerDetails() != null) {

            // Some vendors repeat the manufacturer in model name, let's remove that too
            if (cleanModelName != null && getDetails().getManufacturerDetails().getManufacturer() != null) {
                cleanModelName = cleanModelName.startsWith(getDetails().getManufacturerDetails().getManufacturer())
                        ? cleanModelName.substring(getDetails().getManufacturerDetails().getManufacturer().length()).trim()
                        : cleanModelName.trim();
            }

            if (getDetails().getManufacturerDetails().getManufacturer() != null) {
                sb.append(getDetails().getManufacturerDetails().getManufacturer());
            }
        }

        sb.append((cleanModelName != null && cleanModelName.length() > 0 ? " " + cleanModelName : ""));
        sb.append((cleanModelNumber != null && cleanModelNumber.length() > 0 ? " " + cleanModelNumber.trim() : ""));
        return sb.toString();
    }

    public byte[] getFirstIcon() {
        if (getIcons().length > 0) {
            return getIcons()[0].getData();
        }
        return null;
    }

    public String getLocalURIPrefix() {
        if (getIdentity().getUdn() == null) {
            throw new IllegalStateException("Can't generate local URI prefix without UDN");
        }
        StringBuilder s = new StringBuilder();
        s.append(Constants.RESOURCE_DEVICE_PREFIX);
        s.append("/");
        if (isRoot()) {
            s.append(getIdentity().getUdn().getIdentifierString());
        } else {
            List<Device> devices = new ArrayList();
            Device temp = this;
            while (temp != null) {
                devices.add(temp);
                temp = temp.getParentDevice();
            }
            Collections.reverse(devices);
            for (Device d : devices) {
                if (d == this) continue;
                s.append(d.getIdentity().getUdn().getIdentifierString());
                s.append(Constants.RESOURCE_EMBEDDED_PREFIX);
                s.append("/");
            }
            s.append(getIdentity().getUdn().getIdentifierString());
        }
        return s.toString();
    }

    public URI prefixLocalURI(URI uri) {
        if (!uri.isAbsolute() && !uri.getPath().startsWith("/")) {
            return URI.create(getLocalURIPrefix() + "/" + uri.toString());
        }
        return uri;
    }

    public List<ValidationError> validate() {
        List<ValidationError> errors = new ArrayList();

        if (getType() == null && !getPhase().equals(Phase.DISCOVERED)) {
            errors.add(new ValidationError(
                    getClass(),
                    "deviceType",
                    "Device type/details is required"
            ));
        }

        errors.addAll(getVersion().validate());

        if (getDetails() != null) {
            errors.addAll(getDetails().validate());
        }

        if (hasIcons()) {
            for (Icon icon : getIcons()) {
                errors.addAll(icon.validate());
            }
        }

        if (hasDeviceServices()) {
            for (DeviceService service : getDeviceServices()) {
                errors.addAll(service.validate());
            }
        }

        if (hasEmbeddedDevices()) {
            for (Device embeddedDevice : getEmbeddedDevices()) {
                errors.addAll(embeddedDevice.validate());
            }
        }

        return errors;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Device device = (Device) o;

        if (!identity.equals(device.identity)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return identity.hashCode();
    }

    public abstract D newInstance(UDN udn, UDAVersion version, DeviceType type, DeviceDetails details,
                                  Icon[] icons, DeviceService<S>[] deviceServices, List<D> embeddedDevices) throws ValidationException;

    protected abstract Resource[] discoverResources();

    @Override
    public String toString() {
        return "(" + getClass().getSimpleName() + ") Identity: " + getIdentity().toString() + ", Phase: " + getPhase() + ", Root: " + isRoot();
    }
}
