/*
 * Copyright (c) 2011, 2012, 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.model.types;

import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

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.NVoid;
import com.apple.internal.jobjc.generator.model.types.NType.NStruct;
import com.apple.internal.jobjc.generator.model.types.NType.NField;
import com.apple.internal.jobjc.generator.utils.NTypeMerger;
import com.apple.internal.jobjc.generator.utils.QA;
import com.apple.internal.jobjc.generator.utils.Fp.Pair;
import com.apple.internal.jobjc.generator.utils.NTypeMerger.MergeFailed;

public class Type implements Comparable<Type>{
    public static Type VOID = Type.getType("void", NVoid.inst(), null);
    public static Type VOID_PTR = Type.getType("void*", new NPointer(NVoid.inst()), null);

    final public String name;
    final public NType type32;
    final public NType type64;

    // HACK BS bug where some types have inconsistent definitions in the metadata,
    // e.g. (l / i) in some places and just (l) (and thus (l / l)) in others.
    // This is a mapping from the type name to a Type object with the correct definition.
    private static final Map<String, Type> exceptions;
    static {
        exceptions = new HashMap<String, Type>();
        exceptions.put("OSStatus", getType("OSStatus", new NPrimitive('l'), new NPrimitive('i'))); // (l / i) vs. (l)
        exceptions.put("CGFloat", getType("CGFloat", new NPrimitive('f'), new NPrimitive('d'))); // (f / d) vs. (f)
        exceptions.put("NSRect", getType("NSRect", getNSRectType(), getCGRectType())); // ({{_NSPoint}{_NSSize}} / {{CGPoint}{CGSize}}) vs. ({{_NSPoint}{_NSSize}})
        exceptions.put("NSPoint", getType("NSPoint", getNSPointType(), getCGPointType())); // (_NSPoint / CGPoint) vs. (_NSPoint)
        exceptions.put("NSSize", getType("NSSize", getNSSizeType(), getCGSizeType())); // (_NSSize / CGSize) vs. (_NSSize)
        exceptions.put("NSInteger", getType("NSInteger", new NPrimitive('i'), new NPrimitive('q'))); // (i / q) vs. (i)
        exceptions.put("NSPointArray", getType("NSPointArray", new NPointer(getNSPointType()), new NPointer(getCGPointType()))); // (^_NSPoint / ^CGPoint) vs. (^_NSPoint)
        exceptions.put("NSMultibyteGlyphPacking", getType("NSMultibyteGlyphPacking", new NPrimitive('I'), new NPrimitive('Q'))); // (I / Q) vs. (I)
        exceptions.put("CFTypeRef", getType("CFTypeRef", new NPointer(NVoid.inst()), new NPointer(NVoid.inst()))); // (^v, ^v) vs. (@, @)
    }

    public static Type getType(final String name, final NType t32, final NType t64){
        return TypeCache.inst().pingType(new Type(name, t32, t64));
    }

    private Type(final String name, final NType t32, final NType t64) {
        QA.nonNull(t32);
        this.name = cleanName(name);
        this.type32 = t32;
        this.type64 = t64 == null || t32.equals(t64) ? t32 : t64;
    }

    private JType _getJType;
    public JType getJType() {
        return _getJType!=null ? _getJType : (_getJType = TypeToJType.inst().getJTypeFor(TypeCache.inst().pingType(this)));
    }

    private String _toString;
    @Override public String toString() {
        return _toString != null ? _toString : (_toString = name + " " + new Pair(type32, type64).toString());
    }

    @Override public boolean equals(Object o){
        if(o==null || !(o instanceof Type)) return false;
        Type t = (Type) o;
        return QA.bothNullOrEquals(t.name, this.name)
        && t.type32.equals(this.type32)
        && t.type64.equals(this.type64);
    }

    @Override public int hashCode(){
        return (name == null ? 0 : name.hashCode())
        + type32.hashCode() + type64.hashCode();
    }

    public int compareTo(Type o) { return toString().compareTo(o.toString()); }

    public static Type merge(Type a, Type b) throws MergeFailed{
        if(a!=null && b==null) return a;
        if(a==null && b!=null) return b;
        if(QA.bothNullOrEquals(a, b)) return a;
        if (exceptions.containsKey(a.name)) return exceptions.get(a.name); // HACK BS bug
        if(a.name != null && b.name != null && !a.name.equals(b.name)){
            System.out.println("Merging:");
            System.out.println("\ta.....: " + a.toString());
            System.out.println("\tb.....: " + b.toString());
        }
        final Type merged = new Type(NTypeMerger.inst().mergeName(a.name, b.name),
                NTypeMerger.inst().merge(a.type32, b.type32),
                NTypeMerger.inst().merge(a.type64, b.type64));
        if(a.name != null && b.name != null && !a.name.equals(b.name)){
            System.out.println("\tmerged: " + merged.toString());
        }
        return merged;
    }

    // HACK BS bug where sometimes the name is declared as "id <A, B..." and sometimes it's "id<A,B..."
    public static String cleanName(String name){ return name == null ? null : name.replaceAll("\\s+", ""); }

    // HACK BS bug where NSRect has inconsistent definitions in the metadata
    // Methods return NTypes created according to the correct definitions below:
    //
    // {_NSRect="origin"{_NSPoint="x"f"y"f}"size"{_NSSize="width"f"height"f}} *** 32-bit
    // {CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}} *** 64-bit

    private static NType getCGRectType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("origin", getCGPointType()));
        fields.add(new NField("size", getCGSizeType()));
        return new NStruct("CGRect", fields);
    }

    private static NType getNSRectType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("origin", getNSPointType()));
        fields.add(new NField("size", getNSSizeType()));
        return new NStruct("_NSRect", fields);
    }

    private static NType getCGPointType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("x", new NPrimitive('d')));
        fields.add(new NField("y", new NPrimitive('d')));
        return new NStruct("CGPoint", fields);
    }

    private static NType getNSPointType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("x", new NPrimitive('f')));
        fields.add(new NField("y", new NPrimitive('f')));
        return new NStruct("_NSPoint", fields);
    }

    private static NType getCGSizeType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("width", new NPrimitive('d')));
        fields.add(new NField("height", new NPrimitive('d')));
        return new NStruct("CGSize", fields);
    }

    private static NType getNSSizeType() {
        List<NField> fields = new ArrayList<NField>();
        fields.add(new NField("width", new NPrimitive('f')));
        fields.add(new NField("height", new NPrimitive('f')));
        return new NStruct("_NSSize", fields);
    }
}
