/*
 * 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.brooklyn.util.core.xstream;

import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;

import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.extended.JavaClassConverter;
import com.thoughtworks.xstream.mapper.DefaultMapper;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;

public class XmlSerializer<T> {

    private final Map<String, String> deserializingClassRenames;
    protected final XStream xstream;

    public XmlSerializer() {
        this(ImmutableMap.<String, String>of());
    }
    
    public XmlSerializer(Map<String, String> deserializingClassRenames) {
        this.deserializingClassRenames = deserializingClassRenames;
        this.xstream = new XStream() {
            @Override
            protected MapperWrapper wrapMapper(MapperWrapper next) {
                return XmlSerializer.this.wrapMapperForNormalUsage( super.wrapMapper(next) );
            }
        };
        
        xstream.registerConverter(newCustomJavaClassConverter(), XStream.PRIORITY_NORMAL);
        
        // list as array list is default
        xstream.alias("map", Map.class, LinkedHashMap.class);
        xstream.alias("set", Set.class, LinkedHashSet.class);
        
        xstream.registerConverter(new StringKeyMapConverter(xstream.getMapper()), /* priority */ 10);
        xstream.alias("MutableMap", MutableMap.class);
        xstream.alias("MutableSet", MutableSet.class);
        xstream.alias("MutableList", MutableList.class);
        
        // Needs an explicit MutableSet converter!
        // Without it, the alias for "set" seems to interfere with the MutableSet.map field, so it gets
        // a null field on deserialization.
        xstream.registerConverter(new MutableSetConverter(xstream.getMapper()));
        
        xstream.aliasType("ImmutableList", ImmutableList.class);
        xstream.registerConverter(new ImmutableListConverter(xstream.getMapper()));
        xstream.registerConverter(new ImmutableSetConverter(xstream.getMapper()));
        xstream.registerConverter(new ImmutableMapConverter(xstream.getMapper()));

        xstream.registerConverter(new EnumCaseForgivingConverter());
        xstream.registerConverter(new Inet4AddressConverter());
        
        // See ObjectWithDefaultStringImplConverter (and its usage) for why we want to auto-detect 
        // annotations (usages of this is in the camp project, so we can't just list it statically
        // here unfortunately).
        xstream.autodetectAnnotations(true);
    }

    /**
     * JCC is used when class names are serialized/deserialized and no alias is defined;
     * it is configured in XStream *without* access to the XStream mapper.
     * However we need a few selected mappers (see {@link #wrapMapperForAllLowLevelMentions(Mapper)} )
     * in order to effect renames at the low level, but many of the mappers must NOT be used,
     * e.g. because some might intercept all Class<? extends Entity> references
     * (and that interception is only wanted when serializing <i>instances</i>,
     * as in {@link #wrapMapperForNormalUsage(Mapper)}).
     * <p>
     * This can typically be done simply by registering our own instance (due to order guarantee of PrioritizedList),
     * after the instance added by XStream.setupConverters()
     */
    private JavaClassConverter newCustomJavaClassConverter() {
        return new JavaClassConverter(wrapMapperForAllLowLevelMentions(new DefaultMapper(xstream.getClassLoaderReference()))) {};
    }
    
    /** Adds mappers needed for *any* reference to a class, e.g. when names are used for inner classes, or classes are renamed;
     * this *excludes* basic mentions, however, because most rewrites should *not* be applied at this deep level;
     * mappers which effect aliases or intercept references to entities are usually NOT be invoked in this low-level pathway.
     * See {@link #newCustomJavaClassConverter()}. */
    protected MapperWrapper wrapMapperForAllLowLevelMentions(Mapper next) {
        MapperWrapper result = new CompilerIndependentOuterClassFieldMapper(next);
        Supplier<ClassLoader> classLoaderSupplier = new Supplier<ClassLoader>() {
            @Override public ClassLoader get() {
                return xstream.getClassLoaderReference().getReference();
            }
        };
        return new ClassRenamingMapper(result, deserializingClassRenames, classLoaderSupplier);
    }
    /** Extension point where sub-classes can add mappers wanted when instances of a class are serialized, 
     * including {@link #wrapMapperForAllLowLevelMentions(Mapper)}, plus any usual domain mappings. */
    protected MapperWrapper wrapMapperForNormalUsage(Mapper next) {
        return wrapMapperForAllLowLevelMentions(next);
    }

    public void serialize(Object object, Writer writer) {
        xstream.toXML(object, writer);
    }

    @SuppressWarnings("unchecked")
    public T deserialize(Reader xml) {
        return (T) xstream.fromXML(xml);
    }

    public String toString(T memento) {
        Writer writer = new StringWriter();
        serialize(memento, writer);
        return writer.toString();
    }

    public T fromString(String xml) {
        return deserialize(new StringReader(xml));
    }

}
