/*
 * Copyright 2013 Robert von Burg <eitch@eitchnet.ch>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package li.strolch.model;

import li.strolch.exception.StrolchException;
import li.strolch.exception.StrolchModelException;
import li.strolch.exception.StrolchPolicyException;
import li.strolch.model.Locator.LocatorBuilder;
import li.strolch.model.policy.PolicyDef;
import li.strolch.model.policy.PolicyDefs;
import li.strolch.model.timedstate.StrolchTimedState;
import li.strolch.model.timevalue.IValue;
import li.strolch.model.visitor.StrolchElementVisitor;
import li.strolch.model.xml.StrolchXmlHelper;
import li.strolch.utils.dbc.DBC;

import java.text.MessageFormat;
import java.util.*;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

/**
 * @author Robert von Burg <eitch@eitchnet.ch>
 */
public class Resource extends AbstractStrolchRootElement implements StrolchRootElement, Comparable<Resource> {

	protected Locator locator;
	protected Version version;
	protected Map<String, StrolchTimedState<IValue<?>>> timedStateMap;
	protected PolicyDefs policyDefs;

	/**
	 * Empty constructor - for marshalling only!
	 */
	public Resource() {
		super();
	}

	/**
	 * Default constructor
	 *
	 * @param id   the id
	 * @param name the name
	 * @param type the type
	 */
	public Resource(String id, String name, String type) {
		super(id, name, type);
	}

	@Override
	public void setId(String id) {
		this.locator = null;
		super.setId(id);
	}

	@Override
	public String getObjectType() {
		return Tags.RESOURCE;
	}

	@Override
	public boolean hasVersion() {
		return this.version != null;
	}

	@Override
	public Version getVersion() {
		return this.version;
	}

	@Override
	public void setVersion(Version version) throws IllegalArgumentException, IllegalStateException {
		if (version != null && !getLocator().equals(version.getLocator())) {
			String msg = "Illegal version as locator is not same: Element: {0} Version: {1}";
			throw new IllegalArgumentException(MessageFormat.format(msg, getLocator(), version));
		}

		this.version = version;
	}

	@SuppressWarnings("unchecked")
	public void addTimedState(StrolchTimedState<?> strolchTimedState) {
		assertNotReadonly();
		if (this.timedStateMap == null) {
			this.timedStateMap = new HashMap<>(1, 1.0F);
		}

		if (this.timedStateMap.containsKey(strolchTimedState.getId())) {
			String msg = "A TimedState already exists with id {0} on {1}";
			throw new StrolchException(MessageFormat.format(msg, strolchTimedState.getId(), getLocator()));
		}
		this.timedStateMap.put(strolchTimedState.getId(), (StrolchTimedState<IValue<?>>) strolchTimedState);
		strolchTimedState.setParent(this);
	}

	public <T extends StrolchTimedState<? extends IValue<?>>> T getTimedState(String id) {
		return getTimedState(id, false);
	}

	public <T extends StrolchTimedState<? extends IValue<?>>> T getTimedState(String id, boolean assertExists) {
		if (this.timedStateMap == null) {
			if (assertExists) {
				String msg = "The TimedState {0} does not exist";
				throw new StrolchModelException(MessageFormat.format(msg, getLocator().append(Tags.STATE, id)));
			}
			return null;
		}

		@SuppressWarnings("unchecked") T timedState = (T) this.timedStateMap.get(id);
		if (timedState == null && assertExists) {
			String msg = "The TimedState {0} does not exist";
			throw new StrolchModelException(MessageFormat.format(msg, getLocator().append(Tags.STATE, id)));
		}

		return timedState;
	}

	@SuppressWarnings({"unchecked", "rawtypes"})
	public <T extends StrolchTimedState> T removeTimedState(String id) {
		assertNotReadonly();
		if (this.timedStateMap == null) {
			return null;
		}
		return (T) this.timedStateMap.remove(id);
	}

	public Set<String> getTimedStateKeySet() {
		if (this.timedStateMap == null) {
			return Collections.emptySet();
		}
		return new HashSet<>(this.timedStateMap.keySet());
	}

	public List<StrolchTimedState<? extends IValue<?>>> getTimedStates() {
		if (this.timedStateMap == null) {
			return Collections.emptyList();
		}
		return new ArrayList<>(this.timedStateMap.values());
	}

	/**
	 * Returns a {@link Stream} of all the {@link StrolchTimedState StrolchTimedStates}
	 *
	 * @return the timed states as a stream
	 */
	public Stream<? extends StrolchTimedState<?>> streamOfTimedStates() {
		if (this.timedStateMap == null || this.timedStateMap.isEmpty())
			return Stream.empty();

		return this.timedStateMap.values().stream();
	}

	/**
	 * Returns a {@link Stream} of all the {@link StrolchTimedState StrolchTimedStates} with the given interpretation
	 *
	 * @param interpretation the interpretation for which the timed states are to be returned
	 *
	 * @return the timed states with the given interpretation
	 */
	public Stream<? extends StrolchTimedState<?>> streamOfTimedStatesByInterpretation(String interpretation) {
		if (this.timedStateMap == null || this.timedStateMap.isEmpty())
			return Stream.empty();

		return this.timedStateMap.values().stream().filter(s -> s.getInterpretation().equals(interpretation));
	}

	/**
	 * Returns a {@link Stream} of all the {@link StrolchTimedState StrolchTimedStates} with the given interpretation
	 *
	 * @param interpretation the interpretation for which the timed states are to be returned
	 * @param uom            the uom for which the timed states are to be returned
	 *
	 * @return the timed states with the given interpretation
	 */
	public Stream<? extends StrolchTimedState<?>> streamOfTimedStatesByInterpretationAndUom(String interpretation,
			String uom) {
		if (this.timedStateMap == null || this.timedStateMap.isEmpty())
			return Stream.empty();

		return this.timedStateMap
				.values()
				.stream()
				.filter(s -> s.getInterpretation().equals(interpretation) && s.getUom().equals(uom));
	}

	/**
	 * Returns a list of all the {@link StrolchTimedState StrolchTimedStates} with the given interpretation
	 *
	 * @param interpretation the interpretation for which the timed states are to be returned
	 *
	 * @return the timed states with the given interpretation
	 */
	public List<? extends StrolchTimedState<?>> getTimedStatesByInterpretation(String interpretation) {
		return streamOfTimedStatesByInterpretation(interpretation).collect(toList());
	}

	/**
	 * Returns a list of all the {@link StrolchTimedState StrolchTimedStates} with the given interpretation
	 *
	 * @param interpretation the interpretation for which the timed states are to be returned
	 * @param uom            the uom for which the timed states are to be returned
	 *
	 * @return the timed states with the given interpretation
	 */
	public List<? extends StrolchTimedState<?>> getTimedStatesByInterpretationAndUom(String interpretation,
			String uom) {
		return streamOfTimedStatesByInterpretationAndUom(interpretation, uom).collect(toList());
	}

	public boolean hasTimedStates() {
		return this.timedStateMap != null && !this.timedStateMap.isEmpty();
	}

	public boolean hasTimedState(String id) {
		return this.timedStateMap != null && this.timedStateMap.containsKey(id);
	}

	@Override
	public PolicyDefs getPolicyDefs() {
		if (this.policyDefs == null)
			throw new StrolchPolicyException(getLocator() + " has no Policies defined!");
		return this.policyDefs;
	}

	@Override
	public PolicyDef getPolicyDef(Class<?> clazz) {
		return getPolicyDefs().getPolicyDef(clazz);
	}

	@Override
	public PolicyDef getPolicyDef(String type) {
		return getPolicyDefs().getPolicyDef(type);
	}

	@Override
	public PolicyDef getPolicyDef(Class<?> clazz, PolicyDef defaultDef) {
		if (!hasPolicyDefs())
			return defaultDef;
		return getPolicyDefs().getPolicyDef(clazz, defaultDef);
	}

	@Override
	public PolicyDef getPolicyDef(String type, PolicyDef defaultDef) {
		if (!hasPolicyDefs())
			return defaultDef;
		return getPolicyDefs().getPolicyDef(type, defaultDef);
	}

	@Override
	public boolean hasPolicyDefs() {
		return this.policyDefs != null;
	}

	@Override
	public boolean hasPolicyDef(String type) {
		return this.policyDefs != null && this.policyDefs.hasPolicyDef(type);
	}

	@Override
	public boolean hasPolicyDef(Class<?> clazz) {
		return this.policyDefs != null && this.policyDefs.hasPolicyDef(clazz);
	}

	@Override
	public void setPolicyDefs(PolicyDefs policyDefs) {
		assertNotReadonly();
		this.policyDefs = policyDefs;
		this.policyDefs.setParent(this);
	}

	@Override
	public void addOrUpdate(PolicyDef policyDef) {
		assertNotReadonly();
		DBC.PRE.assertNotNull("policyDef", policyDef);
		if (this.policyDefs == null) {
			this.policyDefs = new PolicyDefs();
			this.policyDefs.setParent(this);
		}
		this.policyDefs.addOrUpdate(policyDef);
	}

	@Override
	public Resource getClone() {
		return getClone(false);
	}

	@Override
	public Resource getClone(boolean withVersion) {
		Resource clone = new Resource();
		super.fillClone(clone);
		clone.locator = this.locator;

		if (this.timedStateMap != null) {
			for (StrolchTimedState<IValue<?>> timedState : this.timedStateMap.values()) {
				clone.addTimedState(timedState.getClone());
			}
		}

		if (this.policyDefs != null)
			clone.policyDefs = this.policyDefs.getClone();
		if (withVersion)
			clone.version = this.version;
		return clone;
	}

	@Override
	public void setReadOnly() {
		if (this.policyDefs != null)
			this.policyDefs.setReadOnly();
		if (this.timedStateMap != null) {
			for (StrolchTimedState<IValue<?>> timedState : this.timedStateMap.values()) {
				timedState.setReadOnly();
			}
		}
		super.setReadOnly();
	}

	@Override
	public Resource ensureModifiable() {
		if (isReadOnly())
			return getClone(true);
		return this;
	}

	@Override
	public Resource ensureReadOnly() {
		if (isReadOnly())
			return this;
		return getClone(true).readOnly();
	}

	@Override
	public Resource readOnly() {
		setReadOnly();
		return this;
	}

	@Override
	public void fillLocator(LocatorBuilder lb) {
		lb.append(Tags.RESOURCE).append(getType()).append(getId());
	}

	@Override
	public Locator getLocator() {
		if (this.locator == null) {
			LocatorBuilder lb = new LocatorBuilder();
			fillLocator(lb);
			this.locator = lb.build();
		}
		return this.locator;
	}

	@Override
	public StrolchElement getParent() {
		return null;
	}

	@Override
	public Resource getRootElement() {
		return this;
	}

	@Override
	public boolean isRootElement() {
		return true;
	}

	@Override
	public <T> T accept(StrolchElementVisitor<T> visitor) {
		return visitor.visitResource(this);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null || getClass() != obj.getClass())
			return false;
		Resource r = (Resource) obj;
		return this.type.equals(r.type) && this.id.equals(r.id);
	}

	@Override
	public int hashCode() {
		return Objects.hash(type, id);
	}

	@Override
	public String toString() {
		if (this.version == null)
			return getLocator().toString();
		return getLocator() + ", Version: " + this.version.getVersion();
	}

	@Override
	public int compareTo(Resource o) {
		return getId().compareTo(o.getId());
	}

	/**
	 * Creates a {@link Locator} for resources of the given type and id
	 *
	 * @param type the type of resource
	 * @param id   the id of the resource
	 *
	 * @return the locator
	 */
	public static Locator locatorFor(String type, String id) {
		return Locator.valueOf(Tags.RESOURCE, type, id);
	}

	/**
	 * Parses the given XML and returns the resource with the given ID
	 *
	 * @param xml the xml to parse
	 * @param id  the id of the resource to return from the parsed elements
	 *
	 * @return the resource, or null if it does not exist
	 */
	public static Resource parse(String xml, String id) {
		return StrolchXmlHelper.parseAndReturnResource(xml, id);
	}
}
