/*
 * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.apple.internal.jobjc.generator.utils;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.apple.internal.jobjc.generator.model.types.NType;
import com.apple.internal.jobjc.generator.model.types.NType.NArray;
import com.apple.internal.jobjc.generator.model.types.NType.NBitfield;
import com.apple.internal.jobjc.generator.model.types.NType.NClass;
import com.apple.internal.jobjc.generator.model.types.NType.NField;
import com.apple.internal.jobjc.generator.model.types.NType.NObject;
import com.apple.internal.jobjc.generator.model.types.NType.NPointer;
import com.apple.internal.jobjc.generator.model.types.NType.NPrimitive;
import com.apple.internal.jobjc.generator.model.types.NType.NSelector;
import com.apple.internal.jobjc.generator.model.types.NType.NStruct;
import com.apple.internal.jobjc.generator.model.types.NType.NUnion;
import com.apple.internal.jobjc.generator.model.types.NType.NUnknown;
import com.apple.internal.jobjc.generator.model.types.NType.NVoid;
import com.apple.internal.jobjc.generator.utils.Fp.Dispatcher;
import com.apple.internal.jobjc.generator.utils.Fp.Map2;
import com.apple.jobjc.JObjCRuntime.Width;

/**
 * Merges two NTypes. All merge does is fill out missing information. It doesn't choose the larger primitive when there's a conflict or anything like that.
 *
 * Example:
 *<pre>
 * a: {_NSRect={_NSPoint="x"f"y"f}"size"{_NSSize=ff}}
 * b: {_NSRect="origin"{_NSPoint=ff}{_NSSize="width"f"height"f}}
 * c: {_NSRect="origin"{_NSPoint="x"f"y"f}"size"{_NSSize="width"f"height"f}}
 *</pre>
 */
public class NTypeMerger {
    public static class MergeFailed extends RuntimeException{
        public MergeFailed(String reason, Object a, Object b){
            super(reason
                    + " -- (" + a.getClass().getSimpleName() + ") a: " + a
                    + " -- (" + b.getClass().getSimpleName() + ") b: " + b);
        }
    }

    private static NTypeMerger INST = new NTypeMerger();
    public static NTypeMerger inst(){ return INST; }

    /**
     * Merge a and b.
     */
    public NType merge(NType a, NType b) throws MergeFailed{
        if(a!=null && b==null) return a;
        if(a==null && b!=null) return b;
        if(a==null && b==null) return null;
        if(a.equals(b)) return a;
        try {
            return Dispatcher.dispatch(getClass(), this, "accept", a, b);
        } catch (NoSuchMethodException e) {
            throw new MergeFailed("a and b are of different NType", a, b);
        }
    }

    private static Collection<String> emptyNames = Arrays.asList(null, "", "?");
    /**
     * Merge two identifiers:
     *  - If they're equal, return one.
     *  - If one is null, "", "?", return the other one.
     *  - else throw MergeFailed
     *
     *  Exception: Due to a bug in BridgeSupport, this will return
     *  a (the first arg) instead of throwing MergeFailed.
     */
    public String mergeName(String a, String b) throws MergeFailed{
        if(QA.bothNullOrEquals(a, b)) return a;
        if(emptyNames.contains(a) && !emptyNames.contains(b)) return b;
        if(emptyNames.contains(b) && !emptyNames.contains(a)) return a;
        return a; // HACK BS bug #5954843
//        throw new MergeFailed("a and b have different names");
    }

    private Map mergeMap(Map a, Map b) throws MergeFailed{
        if(a.equals(b)) return a;
        Map ret = new HashMap();
        Set keys = new HashSet(Fp.append(a.keySet(), b.keySet()));
        for(Object key : keys){
            Object ai = a.get(key);
            Object bi = b.get(key);
            if(ai != null && bi == null) ret.put(key, ai);
            else if(ai == null && bi != null) ret.put(key, bi);
            else if(ai.equals(bi)) ret.put(key, ai);
            else throw new MergeFailed("a and b are different", ai, bi);
        }
        return ret;
    }

    public Map<Width,Integer> mergeSizeOf(Map<Width,Integer> a, Map<Width,Integer> b) throws MergeFailed{
        return mergeMap(a, b);
    }

    public Map<Width,Integer> mergeOffset(Map<Width,Integer> a, Map<Width,Integer> b) throws MergeFailed{
        return mergeMap(a, b);
    }

    //

    private void mustEqual(NType a, NType b){
        if(!a.equals(b)) throw new MergeFailed("a must equal b", a, b);
    }

    protected NType accept(NBitfield a, NBitfield b) {
        mustEqual(a, b);
        return a;
    }

    protected NType accept(NPrimitive a, NPrimitive b) {
        mustEqual(a, b);
        return a;
    }

    protected NType accept(NPointer a, NPointer b) {
        return new NPointer(NTypeMerger.inst().merge(a.subject, b.subject));
    }

    // Merge structs

    protected NField mergeNFields(NField a, NField b) {
        return new NField(mergeName(a.name, b.name),
                NTypeMerger.inst().merge(a.type, b.type),
                mergeOffset(a.offset, b.offset));
    }

    protected NStruct mergeStructs(NStruct a, NStruct b){
        if(a.fields.size() != b.fields.size())
            throw new MergeFailed("a and b have different numbers of fields", a, b);

        List<NField> fields = Fp.map2(new Map2<NField,NField,NField>(){
            public NField apply(NField f32, NField f64) { return mergeNFields(f32, f64); }
        }, a.fields, b.fields);

        return new NStruct(mergeName(a.name, b.name),
                fields, mergeSizeOf(a.sizeof, b.sizeof));
    }

    protected NType accept(NStruct a, NStruct b) {
        return mergeStructs(a, b);
    }

    protected NType accept(NUnion a, NUnion b) {
        NStruct nst = mergeStructs(a, b);
        return new NUnion(nst.name, Fp.map(NUnion.zeroOffsets, nst.fields), nst.sizeof);
    }

    protected NType accept(NArray a, NArray b) {
        if(a.length != b.length)
            throw new MergeFailed("a and b are of different sizes", a, b);
        return new NArray(a.length, NTypeMerger.inst().merge(a.type, b.type));
    }

    protected NType accept(NVoid a, NVoid b) { return NVoid.inst(); }
    protected NType accept(NObject a, NObject b) { return NObject.inst(); }
    protected NType accept(NClass a, NClass b) { return NClass.inst(); }
    protected NType accept(NSelector a, NSelector b) { return NSelector.inst(); }
    protected NType accept(NUnknown a, NUnknown b) { return NUnknown.inst(); }
}
