/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
// https://issues.redhat.com/browse/TEIID-2163
// https://issues.redhat.com/browse/TEIID-3044 (partially, only this class)
// https://issues.redhat.com/browse/TEIID-3175 (partially, only this class)
package org.teiid.metadata;

import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.teiid.core.types.DataTypeManager;
import org.teiid.core.util.EquivalenceUtil;
import org.teiid.core.util.StringUtil;
import org.teiid.core.util.NamesUtil;
import org.teiid.query.sql.StreamPositionAware;


/**
 * AbstractMetadataRecord
 */
public abstract class AbstractMetadataRecord implements Serializable {
	
	public interface Modifiable {
		long getLastModified();
	}
	
	public interface DataModifiable {
		long getLastDataModification();
	}
	
	private static final long serialVersionUID = 564092984812414058L;

	public final static char NAME_DELIM_CHAR = '.';
	
	private static AtomicLong UUID_SEQUENCE = new AtomicLong();
	public static AtomicInteger OID_SEQUENCE = new AtomicInteger();

    private String uuid; //globally unique id
	private Integer oid; //globally unique oid
    private String name; //contextually unique name
    
    private String nameInSource;

	private Map<String, String> properties;
    private Map<String, StreamPositionAware> positions;
	private StreamPositionAware argPos;
	private StreamPositionAware optPos;
	private String annotation;
	private String remark;
	private int thresholdToRemove;
	private transient UUIDChanger uuidChanger;

	public static final String RELATIONAL_URI = "{http://www.teiid.org/ext/relational/2012}"; //$NON-NLS-1$

	public AbstractMetadataRecord() {
		generateOid(1);
	}

	public Integer getOid() {
		return oid;
	}

	protected void generateOid(int delta) {
		assert delta > 0;
		int newOid = OID_SEQUENCE.addAndGet(delta);
		oid = newOid - (delta - 1);
	}

	public String getUUID() {
		if (uuid == null) {
			uuid = String.valueOf(UUID_SEQUENCE.getAndIncrement());
		}
		return uuid;
	}
	
	public void setUUID(String uuid) {
		if (this.uuidChanger != null)
			this.uuidChanger.changeUUID(this.uuid, uuid);
		this.uuid = uuid;
	}
	
	public String getNameInSource() {
		return nameInSource;
	}
	
	public void setNameInSource(String nameInSource) {
		this.nameInSource = DataTypeManager.getCanonicalString(nameInSource);
	}
	
	  /**
     * Get the name in source or the name if
     * the name in source is not set.
     * @return
     */
    public String getSourceName() {
            if (this.nameInSource != null && this.nameInSource.length() > 0) {
                    return this.nameInSource;
            }
            return getName();
    }
	/**
     * WARNING - The name returned by this method may be ambiguous and
     * is not SQL safe - it may need quoted/escaped
     */
	public String getFullName() {
        AbstractMetadataRecord parent = getParent();
        if (parent != null) {
        	String result = parent.getFullName() + NAME_DELIM_CHAR + getName();
        	return result;
        }
        return name;
	}

	public void getSQLString(StringBuilder sb) {
		AbstractMetadataRecord parent = getParent();
		if (parent != null) {
			parent.getSQLString(sb);
			sb.append(NAME_DELIM_CHAR);
		}
		sb.append('"').append(StringUtil.replace(name, "\"", "\"\"")).append('"'); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Get the full name as a SQL safe string
	 * @return
	 */
	public String getSQLString() {
		StringBuilder sb = new StringBuilder();
		getSQLString(sb);
		return sb.toString();
	}
	
	public AbstractMetadataRecord getParent() {
		return null;
	}
	
	public String getName() {
		return name;
	}	
	
	public void setName(String name) {
		this.name = DataTypeManager.getCanonicalString(name);
	}
	
	public String getCanonicalName() {
		return NamesUtil.getCanonicalName(name);
	}
	
    public String toString() {
    	StringBuffer sb = new StringBuffer(100);
        sb.append(getClass().getSimpleName());
        sb.append(" name="); //$NON-NLS-1$
        sb.append(getName());
        sb.append(", nameInSource="); //$NON-NLS-1$
        sb.append(getNameInSource());
        sb.append(", uuid="); //$NON-NLS-1$
        sb.append(getUUID());
        return sb.toString();
    }
    
    /**
     * Return the extension properties for this record - may be unmodifiable
     * if {@link #setProperties(LinkedHashMap)} or {@link #setProperty(String, String)}
     * has not been called.
     * @return
     */
    public Map<String, String> getProperties() {
    	if (properties == null) {
    		return Collections.emptyMap();
    	}
    	return properties;
	}
    
    public String getProperty(String key, boolean checkUnqualified) {
    	String value = getProperties().get(key);
    	if (value != null || !checkUnqualified) {
    		return value;
    	}
    	int index = key.indexOf('}');
    	if (index > 0 && index < key.length() &&  key.charAt(0) == '{') {
    		key = key.substring(index + 1, key.length());
    	}
    	return getProperties().get(key);
    }
    
    /**
     * The preferred setter for extension properties.
     * @param key
     * @param value, if null the property will be removed
     */
    public String setProperty(String key, String value) {
    	if (value == null) {
    		if (this.properties == null) {
    			return null;
    		}
    		return this.properties.remove(key);
    	}
    	if (this.properties == null) {
    		this.properties = new TreeMap<String, String>(StringUtil.CASE_INSENSITIVE_ORDER);
    	}
    	return this.properties.put(DataTypeManager.getCanonicalString(key), DataTypeManager.getCanonicalString(value));
    }
    
    public void setProperties(Map<String, String> properties) {
		if (properties != null) {
			properties = new HashMap<>(properties); // construct a new map for safety in case properties and this.properties is the same instance
		}
    	if (this.properties == null) {
    		this.properties = new TreeMap<String, String>(StringUtil.CASE_INSENSITIVE_ORDER);
    	} else {
    		this.properties.clear();
    	}
		if (properties != null) {
			this.properties.putAll(properties);
		}
	}

    public String getAnnotation() {
		return annotation;
	}
    
    public void setAnnotation(String annotation) {
		this.annotation = DataTypeManager.getCanonicalString(annotation);
	}

	public String getRemark() {
		return remark;
	}

	public void setRemark(String remark) {
		this.remark = remark;
	}

    /**
     * Compare two records for equality.
     */
    public boolean equals(Object obj) {
        if(obj == this) {
            return true;
        }

        if(obj.getClass() != this.getClass()) {
            return false;
        }

        AbstractMetadataRecord other = (AbstractMetadataRecord)obj;

        return EquivalenceUtil.areEqual(this.getUUID(), other.getUUID());
    }
    
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    	in.defaultReadObject();
    	if (this.properties != null && !(this.properties instanceof TreeMap<?, ?>)) {
    		this.setProperties(this.getProperties());
    	}
    }

    public int hashCode() {
        return getUUID().hashCode();
    }

	public boolean isUUIDSet() {
		return this.uuid != null && this.uuid.length() > 0 && !Character.isDigit(this.uuid.charAt(0));
	}

    private boolean invisbleInMetadata = false;

	public boolean isInvisibleInMetadata() {
		return invisbleInMetadata;
	}

	public void setInvisibleInMetadata(boolean invisibleInMetadata) {
		this.invisbleInMetadata = invisibleInMetadata;
	}

	public Map<String, StreamPositionAware> getPositions() {
		if (positions == null)
			positions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
		return positions;
	}

	public StreamPositionAware getArgPos() {
		return argPos;
	}

	public void setArgPos(StreamPositionAware argPos) {
		this.argPos = argPos;
	}

	public StreamPositionAware getOptPos() {
		return optPos;
	}

	public void setOptPos(StreamPositionAware optPos) {
		this.optPos = optPos;
	}

	public List<? extends AbstractMetadataRecord> getChildren() {
		return Collections.emptyList();
	}

	public void recalcWithChildren(int start, int length) {
		if (optPos != null)
			optPos.recalc(start, length);
		if (argPos != null)
			argPos.recalc(start, length);
		if (positions != null)
			for (StreamPositionAware pos : positions.values())
				if (pos != null)
					pos.recalc(start, length);
		for (AbstractMetadataRecord child : getChildren())
			child.recalcWithChildren(start, length);
	}

	public void moveWithChildren(int delta) {
		if (optPos != null)
			optPos.move(delta);
		if (argPos != null)
			argPos.move(delta);
		if (positions != null)
			for (StreamPositionAware pos : positions.values())
				if (pos != null)
					pos.move(delta);
		for (AbstractMetadataRecord child : getChildren())
			child.moveWithChildren(delta);
	}

	public int getThresholdToRemove() {
		return thresholdToRemove;
	}

	public void setThresholdToRemove(int thresholdToRemove) {
		this.thresholdToRemove = thresholdToRemove;
	}

	public void setUuidChanger(UUIDChanger uuidChanger) {
		this.uuidChanger = uuidChanger;
	}

	public interface UUIDChanger {
		void changeUUID(String oldUUID, String newUUID);
	}

	public UUIDKey getUUIDKey() {
		return new UUIDKey(getUUID());
	}

	public static class UUIDKey implements Comparable<UUIDKey> {
		private final String uuid;
		private final int preCalculatedHashCode;

		public UUIDKey(String uuid) {
			this.uuid = uuid;
			this.preCalculatedHashCode = uuid.toLowerCase().hashCode();
		}

		@Override
		public int hashCode() {
			return preCalculatedHashCode;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) return true;
			if (obj == null || getClass() != obj.getClass()) return false;
			UUIDKey other = (UUIDKey) obj;
			// If hash codes are equal, further validate with original values
			return this.preCalculatedHashCode == other.preCalculatedHashCode &&
					this.uuid.equals(other.uuid);
		}

		@Override
		public int compareTo(UUIDKey o) {
			// Primary comparison based on hash code for performance
			int hashComparison = Integer.compare(this.preCalculatedHashCode, o.preCalculatedHashCode);
			if (hashComparison != 0) {
				return hashComparison;
			}
			// Fall back to String comparison if hash codes are equal
			return this.uuid.compareToIgnoreCase(o.uuid);
		}

		public String getUuid() {
			return uuid;
		}
	}
}
