/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.isis.core.metamodel.adapter.oid;

import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.services.bookmark.Bookmark;
import org.apache.isis.core.metamodel.adapter.oid.Oid.State;
import org.apache.isis.core.metamodel.adapter.version.Version;
import org.apache.isis.core.metamodel.spec.ObjectSpecId;

/**
 * Factory for subtypes of {@link Oid}, based on their oid str.
 * 
 * <p>
 * Examples
 * <dl>
 * <dt>CUS:123</dt>
 * <dd>persistent root</dd>
 * <dt>!CUS:123</dt>
 * <dd>transient root</dd>
 * <dt>*CUS:123</dt>
 * <dd>view model root</dd>
 * <dt>CUS:123$items</dt>
 * <dd>collection of persistent root</dd>
 * <dt>!CUS:123$items</dt>
 * <dd>collection of transient root</dd>
 * <dt>CUS:123~NME:2</dt>
 * <dd>aggregated object within persistent root</dd>
 * <dt>!CUS:123~NME:2</dt>
 * <dd>aggregated object within transient root</dd>
 * <dt>CUS:123~NME:2~CTY:LON</dt>
 * <dd>aggregated object within aggregated object within root</dd>
 * <dt>CUS:123~NME:2$items</dt>
 * <dd>collection of an aggregated object within root</dd>
 * <dt>CUS:123~NME:2~CTY:LON$streets</dt>
 * <dd>collection of an aggregated object within aggregated object within root</dd>
 * </dl>
 * 
 * <p>
 * Separators:
 * <dl>
 * <dt>!</dt>
 * <dd>precedes root object type, indicates transient</dd>
 * <dt>*</dt>
 * <dd>precedes root object type, indicates transient</dd>
 * <dt>:</dt>
 * <dd>precedes root object identifier</dd>
 * <dt>~</dt>
 * <dd>precedes aggregate oid</dd>
 * <dt>$</dt>
 * <dd>precedes collection name</dd>
 * <dt>^</dt>
 * <dd>precedes version</dd>
 * </dl>
 * 
 * <p>
 * Note that # and ; were not chosen as separators to minimize noise when URL encoding OIDs.
 */
public final class OidMarshaller {

    public final static OidMarshaller INSTANCE = new OidMarshaller();

    private OidMarshaller(){}

    //region > public constants
    public static final String VIEWMODEL_INDICATOR =
            Bookmark.ObjectState.VIEW_MODEL.getCode(); // "*"
    //endregion

    //region > private constants
    private static final String TRANSIENT_INDICATOR =
            Bookmark.ObjectState.TRANSIENT.getCode() ; // "!"

	private static final String SEPARATOR = ":";
	private static final String SEPARATOR_NESTING = "~";
	private static final String SEPARATOR_COLLECTION = "$";
	private static final String SEPARATOR_VERSION = "^";

	private static final String WORD = "[^" + SEPARATOR + SEPARATOR_NESTING + SEPARATOR_COLLECTION + "\\" + SEPARATOR_VERSION + "#" + "]+";
	private static final String DIGITS = "\\d+";
	
	private static final String WORD_GROUP = "(" + WORD + ")";
	private static final String DIGITS_GROUP = "(" + DIGITS + ")";
    
	private static Pattern OIDSTR_PATTERN = 
            Pattern.compile(
            		"^(" +
            		   "(" +
            		     "([" + TRANSIENT_INDICATOR + VIEWMODEL_INDICATOR + "])?" +
            		     WORD_GROUP + SEPARATOR + WORD_GROUP + 
            		   ")" +
            		   "(" + 
            			 "(" + SEPARATOR_NESTING + WORD + SEPARATOR + WORD + ")*" + // nesting of aggregates
            		   ")" +
            		 ")" +
    		 		 "(" + "[" + SEPARATOR_COLLECTION + "]" + WORD + ")?"  + // optional collection name
    		 		 "(" + 
    		 		     "[\\" + SEPARATOR_VERSION + "]" +  
    		 		     DIGITS_GROUP +                    // optional version digit
    		 		     SEPARATOR + WORD_GROUP + "?" +    // optional version user name 
    		 		     SEPARATOR + DIGITS_GROUP + "?" +  // optional version UTC time
    		 		 ")?" + 
            		 "$");
    //endregion

    //region > join, split
    @Programmatic
    public String joinAsOid(String domainType, String instanceId) {
	    return domainType + SEPARATOR + instanceId;
	}

    @Programmatic
    public String splitInstanceId(String oidStr) {
        final int indexOfSeperator = oidStr.indexOf(SEPARATOR);
        return indexOfSeperator > 0? oidStr.substring(indexOfSeperator+1): null;
    }

    //endregion

    //region > unmarshal

    @Programmatic
    @SuppressWarnings("unchecked")
	public <T extends Oid> T unmarshal(String oidStr, Class<T> requestedType) {
        
        final Matcher matcher = OIDSTR_PATTERN.matcher(oidStr);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Could not parse OID '" + oidStr + "'; should match pattern: " + OIDSTR_PATTERN.pattern());
        }

        final String isTransientOrViewModelStr = getGroup(matcher, 3);
        final State state;
        if("!".equals(isTransientOrViewModelStr)) {
            state = State.TRANSIENT;
        } else if("*".equals(isTransientOrViewModelStr)) {
            state = State.VIEWMODEL;
        } else {
            state = State.PERSISTENT;
        }
        
        final String rootObjectType = getGroup(matcher, 4);
        final String rootIdentifier = getGroup(matcher, 5);
        
        final String aggregateOidPart = getGroup(matcher, 6);
        final List<AggregateOidPart> aggregateOidParts = Lists.newArrayList();
        final Splitter nestingSplitter = Splitter.on(SEPARATOR_NESTING);
        final Splitter partsSplitter = Splitter.on(SEPARATOR);
        if(aggregateOidPart != null) {
            final Iterable<String> tildaSplitIter = nestingSplitter.split(aggregateOidPart);
            for(String str: tildaSplitIter) {
                if(Strings.isNullOrEmpty(str)) {
                    continue; // leading "~"
                }
                final Iterator<String> colonSplitIter = partsSplitter.split(str).iterator();
                final String objectType = colonSplitIter.next();
                final String localId = colonSplitIter.next();
                aggregateOidParts.add(new AggregateOidPart(objectType, localId));
            }
        }
        final String collectionPart = getGroup(matcher, 8);
        final String collectionName = collectionPart != null ? collectionPart.substring(1) : null;
        
        final String versionSequence = getGroup(matcher, 10);
        final String versionUser = getGroup(matcher, 11); 
        final String versionUtcTimestamp = getGroup(matcher, 12);
        final Version version = Version.create(versionSequence, versionUser, versionUtcTimestamp);

        if(collectionName == null) {
            if(aggregateOidParts.isEmpty()) {
                ensureCorrectType(oidStr, requestedType, RootOid.class);
                return (T) new RootOid(ObjectSpecId.of(rootObjectType), rootIdentifier, state, version);
            } else {
                throw new RuntimeException("Aggregated Oids are no longer supported");
            }
        } else {
            final String oidStrWithoutCollectionName = getGroup(matcher, 1);
            
            final String parentOidStr = oidStrWithoutCollectionName + marshal(version);

            RootOid parentOid = this.unmarshal(parentOidStr, RootOid.class);
            ensureCorrectType(oidStr, requestedType, ParentedCollectionOid.class);
            return (T)new ParentedCollectionOid(parentOid, collectionName);
        }
    }



    private static class AggregateOidPart {
        AggregateOidPart(String objectType, String localId) {
            this.objectType = objectType;
            this.localId = localId;
        }
        String objectType;
        String localId;
        public String toString() {
            return SEPARATOR_NESTING + objectType + SEPARATOR + localId;
        }
    }
    

    private <T> void ensureCorrectType(String oidStr, Class<T> requestedType, final Class<? extends Oid> actualType) {
        if(!requestedType.isAssignableFrom(actualType)) {
            throw new IllegalArgumentException("OID '" + oidStr + "' does not represent a " +
            actualType.getSimpleName());
        }
    }

    private String getGroup(final Matcher matcher, final int group) {
        final int groupCount = matcher.groupCount();
        if(group > groupCount) {
            return null;
        }
        final String val = matcher.group(group);
        return Strings.emptyToNull(val);
    }


    //endregion

    //region > marshal
    @Programmatic
    public final String marshal(RootOid rootOid) {
        return marshalNoVersion(rootOid) + marshal(rootOid.getVersion());
    }

    @Programmatic
    public final String marshalNoVersion(RootOid rootOid) {
        final String transientIndicator = rootOid.isTransient()? TRANSIENT_INDICATOR : "";
        final String viewModelIndicator = rootOid.isViewModel()? VIEWMODEL_INDICATOR : "";
        return transientIndicator + viewModelIndicator + rootOid.getObjectSpecId() + SEPARATOR + rootOid.getIdentifier();
    }

    @Programmatic
    public final String marshal(ParentedCollectionOid collectionOid) {
        return marshalNoVersion(collectionOid) + marshal(collectionOid.getVersion());
    }

    @Programmatic
    public String marshalNoVersion(ParentedCollectionOid collectionOid) {
        return collectionOid.getRootOid().enStringNoVersion() + SEPARATOR_COLLECTION + collectionOid.getName();
    }

    @Programmatic
    public final String marshal(Version version) {
        if(version == null) {
            return "";
        }
        final String versionUser = version.getUser();
        return SEPARATOR_VERSION + version.getSequence() + SEPARATOR + Strings.nullToEmpty(versionUser) + SEPARATOR + nullToEmpty(version.getUtcTimestamp());
    }

    private static String nullToEmpty(Object obj) {
        return obj == null? "": "" + obj;
    }
    //endregion

}
