/*
 *
 * COPYRIGHT (C) 2014, FONTEVA, INC.
 * ALL RIGHTS RESERVED.
 * -----------------------------------------------------------------------------
 * ALL INFORMATION CONTAINED HEREIN IS, AND REMAINS THE PROPERTY OF FONTEVA
 * INCORPORATED AND ITS SUPPLIERS, IF ANY. THE INTELLECTUAL AND TECHNICAL
 * CONCEPTS CONTAINED HEREIN ARE PROPRIETARY TO FONTEVA INCORPORATED AND
 * ITS SUPPLIERS AND MAY BE COVERED BY U.S. AND FOREIGN PATENTS, PATENTS IN
 * PROCESS, AND ARE PROTECTED BY TRADE SECRET OR COPYRIGHT LAW. DISSEMINATION
 * OF THIS INFORMATION OR REPRODUCTION OF THIS MATERIAL IS STRICTLY FORBIDDEN
 * UNLESS PRIOR WRITTEN PERMISSION IS OBTAINED FROM FONTEVA, INC.
 * -----------------------------------------------------------------------------
 */

/**
 * The SchemaService class provides built in caching for collecting
 * various Apex Describe information on both SObjects and SObject fields.
 * @author Mac Anderson
 **/

public without sharing class PfmSchemaService {
    /**
     * Cache map of SObjectType to DescribeSObjectResult
     */
    private static Map<SObjectType, DescribeSObjectResult> RESULT_CACHE;

    /**
     * Cache map of SObjectType to Map of String to SObjectField
     */
    private static Map<SObjectType, Map<String, SObjectField>> OBJECT_FIELD_CACHE;

    /**
     * Cache map of SObjectType to Map of String to fieldset
     */
    private static Map<SObjectType, Map<String, Schema.FieldSet>> OBJECT_FIELDSET_CACHE;

    /**
     * Cache map of DescribeSObjectResult to Map of String,DescribeFieldResult
     */
    private static Map<DescribeSObjectResult, Map<String, DescribeFieldResult>> FIELD_RESULT_CACHE;

    /**
     * Cache map of SObjectField to DescribeFieldResult
     */
    private static Map<SObjectField, DescribeFieldResult> FIELD_TOKEN_RESULT_CACHE;

    /**
     * Cache map of SObjectType to List<ChildRelationship>
     */
    private static Map<SObjectType, List<ChildRelationship>> CHILD_RELATIONSHIP_CACHE;

    /**
     * Static block to initialize cache in
     * execution context.
     */
    static {
        RESULT_CACHE = new Map<SObjectType, DescribeSObjectResult>();
        OBJECT_FIELD_CACHE = new Map<SObjectType, Map<String, SObjectField>>();
        OBJECT_FIELDSET_CACHE = new Map<SObjectType, Map<String, Schema.FieldSet>>();
        FIELD_RESULT_CACHE = new Map<DescribeSObjectResult, Map<String, DescribeFieldResult>>();
        FIELD_TOKEN_RESULT_CACHE = new Map<SObjectField, DescribeFieldResult>();
        CHILD_RELATIONSHIP_CACHE = new Map<SObjectType, List<ChildRelationship>>();
    }

    /**
     * Returns a token for the SObjectType found by the
     * string value of the SObject's api name.
     * @param objectName String value of the SObject API name
     * @throws SchemaServiceException when an invalid api name is passed for a given SObjectType
     * @return SObjectType
     */
    public static SObjectType getSObjectType(String objectName) {
        if (String.isBlank(objectName)) {
            throw new SchemaServiceException(objectName + ' Cannot Be Empty');
        } else if (Type.forName(objectName) == null) {
            throw new SchemaServiceException(objectName + ' Not Found');
        }
        return ((SObject) Type.forName(objectName).newInstance()).getSObjectType();
    }

    /**
     * Returns a DescribeSObjectResult object for a given SObjectType
     * token.
     * @param  objectToken  The SObjectType to return the
     *                      describe result for
     * @return DescribeSObjectResult
     */
    public static DescribeSObjectResult getDescribe(SObjectType objectToken) {
        if (RESULT_CACHE.get(objectToken) == null)
            RESULT_CACHE.put(objectToken, objectToken.getDescribe());
        return RESULT_CACHE.get(objectToken);
    }

    /**
     * Returns a map with key of the field api name (without namespace if same as pkg)
     * and value of the Schema.Fieldset.
     * @param  token  SObjectType token to return the field map for.
     * @return  Map<String, Schema.Fieldset>
     */
    public static Map<String, Schema.Fieldset> getFieldSetMap(SObjectType token) {
        if (OBJECT_FIELDSET_CACHE.get(token) == null) {
            DescribeSObjectResult objectDescribe = getDescribe(token);
            Map<String, Schema.Fieldset> fieldsetMap = objectDescribe.fieldSets.getMap();
            OBJECT_FIELDSET_CACHE.put(token, fieldsetMap);
        }
        return OBJECT_FIELDSET_CACHE.get(token);
    }

    /**
     * Returns a map with key of the fieldset api name (without namespace if same as pkg)
     * and value of the fildset.
     * @param  token  SObjectType token to return the field map for.
     * @return  Map<String,SObjectField>
     */
    public static Map<String, SObjectField> getFieldMap(SObjectType token) {
        if (OBJECT_FIELD_CACHE.get(token) == null) {
            DescribeSObjectResult objectDescribe = getDescribe(token);
            Map<String, SObjectField> fieldMap = objectDescribe.fields.getMap();
            OBJECT_FIELD_CACHE.put(token, fieldMap);
        }
        return OBJECT_FIELD_CACHE.get(token);
    }

    /**
     * Returns the token for a SObject field from the SObjectType 'token' and
     * a String 'fieldName'.
     * @param token
     * @param fieldName
     * @throws SchemaServiceException when no field is found by the fieldName argument
     * @return SObjectField
     */
    public static SObjectField getSObjectField(SObjectType token, String fieldName) {
        Map<String, SObjectField> fieldMap = getFieldMap(token);
        if (fieldMap.get(fieldName) == null) {
            throw new SchemaServiceException(
                token.getDescribe().getName() +
                ' Object - Field ' +
                fieldName +
                ' Not Found'
            );
        }
        return fieldMap.get(fieldName);
    }

    /**
     * Returns the DescribeFieldResult object for a given
     * SObjectField
     * @param field SObjectField to describe
     * @return DescribeFieldResult
     */
    public static DescribeFieldResult getDescribe(SObjectField field) {
        if (FIELD_TOKEN_RESULT_CACHE.get(field) == null)
            FIELD_TOKEN_RESULT_CACHE.put(field, field.getDescribe());
        return FIELD_TOKEN_RESULT_CACHE.get(field);
    }

    /**
     * Return a field describe for a given object and field by string value for
     * each argument (objectName and fieldName)
     * @param objectName The api name for the SObject type that contains the field to describe
     * @param fieldName The api name for the field to describe
     * @return DescribeFieldResult
     */
    public static DescribeFieldResult getDescribe(String objectName, String fieldName) {
        SObjectType token = getSObjectType(objectName);
        return getDescribe(token, fieldName);
    }

    /**
     * Returns a DescribeFieldResult object for a given SObjectType and field
     * name (String value)
     * @param  objectToken  SObjectType for the SObject that holds the
     *                      field to return the describe result for.
     * @param  fieldName    String value of the api name for the field to return
     *                      the describe result for.
     * @return  DescribeFieldResult
     */
    public static DescribeFieldResult getDescribe(SObjectType objectToken, String fieldName) {
        // this map helps us not lose existing field describe result cache
        Map<String, DescribeFieldResult> cloneMap = new Map<String, DescribeFieldResult>();
        // Do we have describe field results for this sobject?
        // fieldName = fieldName.removeStart(nsp);
        if (FIELD_RESULT_CACHE.get(getDescribe(objectToken)) == null) {
            cloneMap.put(fieldName, getSObjectField(objectToken, fieldName).getDescribe());
            FIELD_RESULT_CACHE.put(getDescribe(objectToken), cloneMap);
        } else if (FIELD_RESULT_CACHE.get(getDescribe(objectToken)).get(fieldName) == null) {
            // our field results do not include the right field result
            // lets make sure we get a clone of the other fields before we kill the map and
            // lose the cache
            cloneMap = FIELD_RESULT_CACHE.get(getDescribe(objectToken)).clone();
            // lets add the new field describe result to the clones map
            cloneMap.put(fieldName, getSObjectField(objectToken, fieldName).getDescribe());
            // lets update the existing field result cache value for the key
            // of this objectTokens describe with the new string/describe field cache
            FIELD_RESULT_CACHE.put(getDescribe(objectToken), cloneMap);
        }
        return FIELD_RESULT_CACHE.get(getDescribe(objectToken)).get(fieldName);
    }

    /**
     * Returns a list of SObject field tokens for a given SObjectType
     * Checks accessiblity of the field for the running user (only includes
     * fields that the running user has access too)
     * @param token SObjectType
     * @return List<SObjectField>
     */
    public static List<SObjectField> getFieldList(SObjectType token) {
        List<SObjectField> results = new List<SObjectField>();
        for (SObjectField field : getFieldMap(token).values()) {
            DescribeFieldResult dfr = getDescribe(field);
            if (
                dfr.isAccessible() &&
                dfr.getName().toLowerCase() != 'connectionreceivedid' &&
                dfr.getName().toLowerCase() != 'connectionsentid'
            ) {
                results.add(field);
            }
        }
        return results;
    }

    /**
     * Returns a list of SObject field tokens for a given SObjectType
     * Checks accessiblity of the field for the running user (only includes
     * fields that the running user has access too)
     * @param token SObjectType
     * @return List<SObjectField>
     */
    public static List<SObjectField> getFieldList(SObjectType token, Boolean allFields) {
        List<SObjectField> results = new List<SObjectField>();
        for (SObjectField field : getFieldMap(token).values()) {
            DescribeFieldResult dfr = getDescribe(field);
            if (
                dfr.getName().toLowerCase() == 'connectionreceivedid' ||
                dfr.getName().toLowerCase() == 'connectionsentid'
            ) {
                continue;
            }
            if (dfr.isAccessible() || allFields)
                results.add(field);
        }
        return results;
    }

    /**
     * Returns a list of child relationships for a given SObjectType
     * @param token SObjectType to return child relationships for
     * @return List<ChildRelationship>
     */
    public static List<ChildRelationship> getChildren(SObjectType token) {
        if (CHILD_RELATIONSHIP_CACHE.get(token) == null) {
            DescribeSObjectResult parent = getDescribe(token);
            List<ChildRelationship> results = new List<ChildRelationship>();
            for (ChildRelationship cr : parent.getChildRelationships())
                results.add(cr);
            CHILD_RELATIONSHIP_CACHE.put(token, results);
        }
        return CHILD_RELATIONSHIP_CACHE.get(token);
    }

    /**
     * Returns the name of a relationship in string form - used to create dynamic
     * queries in the Selector class
     * @param parentObjectType SObjectType token for the parent SObject in the relationship
     * @param childField SObjectField for the child SObject field that links the two SObjectTypes together
     */
    public static String getRelationshipNameToString(SObjectType parentObjectType, SObjectField childField) {
        Boolean relationshipFound = false;
        for (ChildRelationship cr : getChildren(parentObjectType)) {
            if (cr.getField() == childField) {
                relationshipFound = true;
                return cr.getRelationshipName();
            }
        }
        if (!relationshipFound) {
            throw new SchemaServiceException(
                parentObjectType.getDescribe().getName() +
                ' Relationship ' +
                childField.getDescribe().getName() +
                ' Not Found'
            );
        }

        return '';
    }

    public static Boolean checkObjectAccess(
        SObjectType objectType,
        Boolean isInsert,
        Boolean isUpdate,
        Boolean isDelete
    ) {
        try {
            DescribeSObjectResult objectResult = getDescribe(objectType);
            if (isInsert && !objectResult.isCreateable()) {
                return false;
            }
            if (isUpdate && !objectResult.isUpdateable()) {
                return false;
            }
            if (isDelete && !objectResult.isDeletable()) {
                return false;
            }
            if (isInsert || isUpdate) {
                Map<String, SObjectField> fields = getFieldMap(objectType);
                for (String fieldName : fields.keySet()) {
                    SObjectField objectField = fields.get(fieldName);
                    Map<String, DescribeFieldResult> cloneMap = new Map<String, DescribeFieldResult>();
                    if (FIELD_RESULT_CACHE.containsKey(objectResult)) {
                        cloneMap = FIELD_RESULT_CACHE.get(objectResult);
                    }
                    if (!cloneMap.containsKey(fieldName)) {
                        cloneMap.put(fieldName, fields.get(fieldName).getDescribe());
                        FIELD_RESULT_CACHE.put(objectResult, cloneMap);
                    }
                    DescribeFieldResult fieldResult = cloneMap.get(fieldName);
                    if (fieldResult.getType() == Schema.DisplayType.LOCATION) {
                        // Location field has compound fields, which will be checked instead
                        // Location should be skipped as it's non creatable
                        continue;
                    }
                    if (
                        fieldResult.isAccessible() &&
                        fieldResult.isCustom() &&
                        !fieldResult.isCalculated() &&
                        !fieldResult.isAutoNumber()
                    ) {
                        if (isInsert && !fieldResult.isCreateable()) {
                            return false;
                        }
                        if (isUpdate && !fieldResult.isUpdateable()) {
                            return false;
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw e;
        }

        return true;
    }

    public class SchemaServiceException extends Exception {
    }
}
