TypeName.java

package Hoot.Runtime.Names;

import java.util.*;
import java.lang.reflect.Type;
import static java.lang.Character.*;

import Hoot.Runtime.Notes.Decor;
import Hoot.Runtime.Faces.Named;
import Hoot.Runtime.Maps.Library;
import Hoot.Runtime.Faces.Logging;
import Hoot.Runtime.Behaviors.Scope;
import Hoot.Runtime.Behaviors.Typified;
import Hoot.Runtime.Notes.DetailedType;

import static Hoot.Runtime.Functions.Exceptional.*;
import static Hoot.Runtime.Functions.Utils.*;
import static Hoot.Runtime.Faces.Logging.*;
import static Hoot.Runtime.Names.Operator.*;

import static Hoot.Runtime.Names.Keyword.Array;
import static Hoot.Runtime.Names.Keyword.Arrayed;
import static Hoot.Runtime.Names.Keyword.Behaviors;
import static Hoot.Runtime.Names.Keyword.Collections;
import static Hoot.Runtime.Names.Keyword.TypeSuffix;
import static Hoot.Runtime.Names.Keyword.Object;
import static Hoot.Runtime.Names.Keyword.Hoot;
import static Hoot.Runtime.Names.Name.removeTail;
import static Hoot.Runtime.Names.Primitive.Blank;
import static Hoot.Runtime.Notes.Decor.Generic;

/**
 * Identifies a type, including (possibly) its package.
 *
 * @author nik <nikboyd@sonic.net>
 * @see "Copyright 2010,2021 Nikolas S Boyd."
 * @see "Permission is granted to copy this work provided this copyright statement is retained in all copies."
 */
public class TypeName implements Named {

    private TypeName(List<String> heads, List<String> tails) { headNames.addAll(heads); tailNames.addAll(tails); }
    private TypeName(TypeName source) {
        this(source.headNames, source.tailNames); decor.makeNote(source.decor.notes()); makeArrayed(source.isArrayed()); }

    private static TypeName from(Typified type) { return fromCache(type.fullName()); }
    private static TypeName from(Named item) { return fromName(item.fullName()); }
    public static TypeName from(Class<?> aClass) {
        if (aClass == null) return EmptyType;
        return aClass.getName().contains(Dollar) ?
            TypeName.fromName(aClass.getName()) :
            TypeName.fromName(aClass.getCanonicalName()); }

    public static TypeName fromOther(Object item) {
        if (item == null) return EmptyType;
        if (item instanceof TypeName) return new TypeName((TypeName)item);
        if (item instanceof Typified) return TypeName.from((Typified)item);
        if (item instanceof Named) return TypeName.from((Named)item);
        return TypeName.from(item.getClass()); }

    public static TypeName withTails(List<String> names) { return new TypeName(emptyList(String.class), copyList(names)); }
    public static List<TypeName> listFrom(List<String> names) { return map(names, n -> fromName(n)); }
    public static TypeName fromType(Type aType) { return DetailedType.from(aType).toTypeName(); }
    public static TypeName fromTail(String typeName) { return withTails(wrap(typeName.split(WildDot))); }
    public static TypeName fromName(String typeName) { return with(typeName); }
    public static TypeName with(String... names) { return with(wrap(names)); }

    static final String TypeTail = " type";
    static final String ClassTail = " class";
    public static TypeName with(List<String> names) {
        if (names.isEmpty()) return EmptyType;
        if (1 == names.size()) {
            String fullName = names.get(0);
            if (fullName.contains(Dot)) {
                names = wrap(fullName.split(WildDot));
            }
            else if (names.get(0).contains(Blank)) {
                if (fullName.endsWith(TypeTail)) {
                    fullName = fullName.substring(0, fullName.length() - TypeTail.length());
                }
                if (fullName.endsWith(ClassTail)) {
                    fullName = fullName.substring(0, fullName.length() - ClassTail.length());
                }
                names = wrap(fullName.split(Blank));
            }
        }

        ArrayList<String> heads = copyList(names);
        String tail = heads.remove(heads.size() - 1);

        if (tail.equals(Wild + Wild)) {
            tail = heads.remove(heads.size() - 1);
            String[] tails = { tail, Wild };
            return new TypeName(heads, wrap(tails));
        }

        boolean arrayed = tail.endsWith(Arrayed);
        if (arrayed) tail = removeTail(tail, Arrayed);
        String[] tails = tail.split(Dollar);

        return new TypeName(heads, wrap(tails)).makeArrayed(arrayed); }

    // look for known typical Smalltalk variable naming conventions
    private static final String[] Prefixes = { "other", "another", "an", "a", };
    public static TypeName inferFrom(String variableName) { return inferFrom(variableName, null); }
    public static TypeName inferFrom(String variableName, Scope s) {
        if (isEmpty(variableName)) return RootType();
        if (hasSome(s) && s.isFacial()) {
            if (Typified.from(s).matchesTail(variableName))
                return TypeName.fromName(s.name());
        }
        for (String prefix : Prefixes) {
            if (variableName.startsWith(prefix)) {
                String result = variableName.substring(prefix.length());
                if (isCapitalized(result)) return TypeName.fromName(result);
            }
        }
        return RootType();
    }


    public Global toGlobal() { return Global.withList(allNames()).makeArrayed(arrayedType); }
    public List<String> allNames() {
        ArrayList<String> results = emptyList(String.class);
        results.addAll(headNames);
        results.addAll(tailNames);
        return results; }

    private final Decor decor = new Decor();
    public TypeName withNotes(List<String> notes) { decor.makeNote(notes); return this; }
    public TypeName withNotes(String... notes) { return this.withNotes(wrap(notes)); }
    public boolean isNotedPrimitive() { return !this.isUnknown() && decor.hasPrimitive(); }
    public boolean isNotedGeneric() { return !this.isUnknown() && decor.hasGeneric(); }
    public TypeName noteGeneric() { return this.withNotes(Generic); }

    private final List<String> headNames = emptyList(String.class);
    protected int headCount() { return headNames.size(); }
    public boolean isUnpackaged() { return headNames.isEmpty(); }
    protected String headMost() { return (this.isUnpackaged()) ? Empty : headNames.get(0); }
    public boolean fromElementaryPackage() { return Primitive.fromElementaryPackage(headMost() + Dot); }

    private final List<String> tailNames = emptyList(String.class);
    protected int tailCount() { return tailNames.size(); }
    protected String tailMost() { return tailNames.isEmpty() ? Empty : tailNames.get(tailCount() - 1); }
    private static boolean isCapitalized(String name) { return !isEmpty(name) && isUpperCase(name.charAt(0)); }

    protected boolean arrayedType = false;
    public boolean isArrayed() { return arrayedType; }
    public TypeName makeArrayed(boolean value) { this.arrayedType = value; return this; }

    public boolean isAny() { return Any.equals(tailMost()); }
    public boolean isWild() { return Wild.equals(tailMost()); }
    public boolean isUnknown() { return tailMost().isEmpty(); }
    public boolean isNested() { return !this.isUnknown() && tailCount() > 1; }
    public boolean isGeneric() { return isGenericType() || isNotedGeneric(); }
    public boolean isGenericType() { return tailMost().endsWith(TypeSuffix); }

    static final String SigsReport = "types %s %s %s %s";
    protected boolean reportMatch(TypeName type, boolean matched) {
        String similarity = (matched ? "~=" : "!=");
        String realName = Name.asMetaMember(tailName());
        String gen = (isGeneric() || type.isGeneric() ? "generically" : "");
        whisper(format(SigsReport, realName, similarity, type.shortName(), gen));
        return matched;
    }

    public boolean matches(TypeName match) {
        boolean result = false;
        if (isArrayed() && !match.isArrayed()) {
            return reportMatch(match, false);
        }

        if (!isArrayed() && match.isArrayed()) {
            return reportMatch(match, false);
        }

        String realName = Name.asMetaMember(tailName());
        if (isGeneric() && match.isGeneric()) {
            return reportMatch(match, true);
        }

        if (isUnpackaged()) {
            result = match.tailName().startsWith(realName);
            if (result) { reportMatch(match, true); }
            result = realName.equals(match.tailName());
            return reportMatch(match, result);
        }

        if (match.isUnpackaged()) {
            result = realName.startsWith(match.tailName());
            if (result) { reportMatch(match, true); }
            result = realName.equals(match.tailName());
            return reportMatch(match, result);
        }

        realName = Name.asMetaMember(typeName());
        result = match.tailName().startsWith(realName);
        if (result) { reportMatch(match, true); }
        result = realName.equals(match.typeName());
        return reportMatch(match, result);
    }

    protected boolean matches(Named item) { return matches(item.fullName()); }
    public boolean matches(String match) {
        return !isEmpty(match) && match.equals(match.contains(Dot) ? fullName() : shortName()); }

    @Override public int hashCode() { return fullName().hashCode(); }
    @Override public boolean equals(Object candidate) {
        if (hasNone(candidate)) return false;
        if (candidate instanceof TypeName) return matches((TypeName)candidate);
        if (candidate instanceof String) return matches((String)candidate);
        if (candidate instanceof Named) return matches((Named)candidate);
        return false;
    }

    public static Class<?> findPrimitiveClass(String name) { return nullOrTryQuietly(n -> Class.forName(n), name); }
    public Class<?> findPrimitiveClass() { return findPrimitiveClass(fullName()); }
    public Class<?> findClass() { return nullOr(f -> f.primitiveClass(), findType()); }
    public Typified findType() {
        if (this.isAny() || this.isWild() || this.isUnknown()) return null;
        if (this.isGeneric()) return ObjectType().findType();
        return Library.findFace(typeName()); }

    @Override public String name() { return fullName(); }
    @Override public String shortName() { return tailName(); }
    @Override public String fullName() {
        if (this.isUnknown()) return RootType().fullName();
        String packageName = packageName();
        return packageName.isEmpty() ? tailName() :
                Name.formType(packageName, tailName()); }

    public String hootPackage() { return packageName().replace(Dot, Blank); }
    public String hootName() {
        String fullName = fullName();
        if (!Name.isMetaNamed(fullName)) return fullName.replace(Dot, Blank);
        String tail = Name.isMetaclassNamed(fullName)? " class" : " type";
        return Name.withoutMeta(fullName).replace(Dot, Blank) + tail; }

    public String javaName() {
        String fullName = fullName();
        return fullName.replace(Blank, Dot);
    }

    public String convertedTail() { return Primitive.convertType(packagedTail()); }
    public String packagedTail() {
        String packageName = packageName();
        return packageName.isEmpty() ? markedUpTail() :
                Name.formType(packageName, markedUpTail()); }


    private String tailMark() { return isWild() ? Dot : Dollar; }
    public String markedUpTail() { return joinWith(tailMark(), tailNames); }

    private String trailer() { return isArrayed() ? Arrayed : Empty; }
    private String tailName() { return markedUpTail() + trailer(); }

    public String typeName() {
        if (isUnknown()) return RootType().shortName();

        String fullName = fullName();
        if (Primitive.convertsType(fullName)) {
            return Primitive.convertType(fullName);
        }

        if (!isNested()) {
            if (isAny()) return Quest;
            if (isPrimitiveType()) return tailName().toLowerCase();
        }

        return fullName;
    }

    public String packageName() {
        String packageName = Name.formType(headNames);
        return this.isUnpackaged() ? Empty : fromElementaryPackage() ? packageName.toLowerCase() : packageName; }

    public boolean isObjectType() { return this.matches(ObjectType()); }
    public boolean isRootType() { return this.matches(RootType()); }
    public boolean isPrimitiveType() {
        if (this.isUnknown()) return false;
        if (isUnpackaged()) return Primitive.isPrimitiveType(tailMost());
        if (isElementaryType() || !fromElementaryPackage()) return false;
        return Primitive.isPrimitiveType(tailMost()); }

    public boolean isElementaryType() { return Primitive.isElementaryType(fullName()); }
    public boolean isEraseableType() { return Primitive.isEraseableType(tailMost()); }

    public static TypeName fromCache(String... names) {
        if (hasNo(names)) return EmptyType;
        TypeName test = TypeName.with(names);
        String typeName = test.typeName();
        if (!TypeCache.containsKey(typeName) && !test.isUnpackaged()) {
            TypeCache.put(typeName, test);
        }
        return TypeCache.get(typeName);
    }

    private static final String Quest = "?";
    private static final String Any = "Any";
    private static final String Wild = "*";
    private static final String Dollar = "$";

    public static final Class<?> JavaRoot = java.lang.Object.class;
    public static final TypeName ObjectType = TypeName.from(JavaRoot);
    public static final TypeName VoidType = TypeName.from(void.class);
    public static final TypeName EmptyType = new TypeName(emptyList(String.class), emptyList(String.class));
    static final HashMap<String, TypeName> TypeCache = emptyMap(TypeName.class);
    static {
        TypeCache.put(Empty, EmptyType);
        TypeCache.put(VoidType.typeName(), VoidType);
        TypeCache.put(ObjectType.typeName(), ObjectType);
    }

    public static TypeName ObjectType() { return TypeName.fromCache(JavaRoot.getCanonicalName()); }
    public static TypeName RootType() { return TypeName.fromCache(Hoot, Behaviors, Object); }
    public static TypeName ArrayType() { return TypeName.fromCache(Hoot, Collections, Array); }

    /**
     * Resolves a type from its references.
     */
    public static interface Resolver extends Logging {

        public TypeName resolveTypeNamed(Named reference);

    } // Resolver

} // TypeName