public with sharing class FormatUtils {
    public static final String EMAIL_REGEX =
        '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:' +
        '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';

    /**
     * @description Adds a new value to a Text/Picklist/Multipicklist field if it is not already
     *  there separating each value by ";"
     * @param  sObj       SObject record
     * @param  field      field api name
     * @param  valueToAdd value to be added to field param
     */
    public static void addToMultiValueField(SObject sObj, String field, String valueToAdd) {
        addToMultiValueField(sObj, field, new List<String>{ valueToAdd });
    }

    /**
     * @description Adds a new value to a Text/Picklist/Multipicklist field if it is not already
     *  there separating each value by ";"
     * @param  sObj       SObject record
     * @param  field      field api name
     * @param  valuesToAdd values to be added to field param
     */
    public static void addToMultiValueField(SObject sObj, String field, List<String> valuesToAdd) {
        if (sObj != null) {
            if (sObj.isSet(field)) {
                String currentValue = String.valueOf(sObj.get(field));
                if (String.isBlank(currentValue)) {
                    sObj.put(field, String.join(valuesToAdd, ';'));
                } else {
                    List<String> currentValues = currentValue.split(';');
                    for (String valueToAdd : valuesToAdd) {
                        if (!currentValues.contains(valueToAdd)) {
                            currentValues.add(valueToAdd);
                            sObj.put(field, String.join(currentValues, ';'));
                        }
                    }
                }
            } else {
                sObj.put(field, String.join(valuesToAdd, ';'));
            }
        }
    }

    /**
     * @description Creates a csv Blob from a List of SObjects
     * @param List<SObject>  SObject records
     * @return Blob     csv Blob created from the SObject records provided
     */
    public static Blob createCsvBlobFromSObjectList(List<SObject> records) {
        return createCsvBlobFromSObjectList(records, false);
    }

    /**
     * @description Creates a csv Blob from a List of SObjects
     * @param List<SObject>  SObject records
     * @param Boolean   whether we should include or ignore the ID property
     * @return Blob     csv Blob created from the SObject records provided
     */
    public static Blob createCsvBlobFromSObjectList(List<SObject> records, Boolean ignoreIds) {
        Blob csvBlob = null;
        if (records != null && !records.isEmpty()) {
            List<String> file = new List<String>();
            List<String> fieldsKeySet = new List<String>(records[0].getPopulatedFieldsAsMap().keySet());
            List<String> fields = new List<String>();
            if (ignoreIds) {
                for (String field : fieldsKeySet) {
                    if (!field.equalsIgnoreCase('ID')) {
                        fields.add(field);
                    }
                }
            } else {
                fields = fieldsKeySet;
            }
            file.add(String.join(fields, ','));
            for (SObject record : records) {
                List<String> line = new List<String>();
                Map<String, Object> fieldsToValue = record.getPopulatedFieldsAsMap();
                for (String field : fields) {
                    if (!ignoreIds || !field.equalsIgnoreCase('ID')) {
                        String fieldValue = String.valueOf(fieldsToValue.get(field));
                        line.add(fieldValue != null ? fieldValue.escapeCsv() : fieldValue);
                    }
                }
                file.add(String.join(line, ','));
            }
            csvBlob = Blob.valueOf(String.join(file, '\n'));
        }
        return csvBlob;
    }

    /**
     * @description Formats an integer
     * @param   bytes       Integer value
     * @return  String      Number + unit
     */
    public static String formatSizeFromBytes(Integer bytes) {
        if (bytes < 1024) {
            return Integer.valueOf(bytes).format() + ' bytes';
        } else if (bytes / 1024 < 1000) {
            return Integer.valueOf(bytes / 1024).format() + ' KB';
        } else {
            return ((bytes / 1024.0) / 1024.0).setScale(2, RoundingMode.HALF_UP) + ' MB';
        }
    }

    /**
     * @description Returns active values from a PicklistEntry list
     * @param   picklistEntries     Schema.PicklistEntry list
     * @return  List<String>        active values
     */
    public static List<String> getActivePicklistValues(List<Schema.PicklistEntry> picklistEntries) {
        List<String> activeValues = new List<String>();
        for (Schema.PicklistEntry pEntry : picklistEntries) {
            if (pEntry.isActive()) {
                activeValues.add(pEntry.getLabel());
            }
        }
        return activeValues;
    }

    /**
     * @description Returns active values from a PicklistEntry list
     * @param   picklistEntries     Schema.PicklistEntry list
     * @return  Map<String, String> API - Label Map
     */
    public static Map<String, String> getActivePicklistValuesMap(List<Schema.PicklistEntry> picklistEntries) {
        Map<String, String> activeValuesMap = new Map<String, String>();
        for (Schema.PicklistEntry pEntry : picklistEntries) {
            if (pEntry.isActive()) {
                activeValuesMap.put(pEntry.getValue(), pEntry.getLabel());
            }
        }
        return activeValuesMap;
    }
    /**
     * PRE-EXISTING Method (Not Reviewed)
     * @description Transforms a csv String into a List of lines of values
     * @param   String  file content
     * @param   Boolean whether to include the header or not
     * @return  List<List<String>>  List of lines, each one being a list of cells
     */
    public static List<List<String>> parseCSV(String contents, Boolean skipHeaders) {
        List<List<String>> allFields = new List<List<String>>();
        // replace instances where a double quote begins a field containing a comma
        // in this case you get a double quote followed by a doubled double quote
        // do this for beginning and end of a field
        //contents = contents.replaceAll(',"""', ',"DBLQT').replaceall('""",', 'DBLQT",');
        contents = contents.replace(',"""', ',"DBLQT').replace('""",', 'DBLQT",');
        // now replace all remaining double quotes - do this so that we can reconstruct
        // fields with commas inside assuming they begin and end with a double quote
        //contents = contents.replaceAll('""', 'DBLQT');
        contents = contents.replace('""', 'DBLQT');
        // we are not attempting to handle fields with a newline inside of them
        // so, split on newline to get the spreadsheet rows
        List<String> lines = new List<String>();
        try {
            lines = safeSplit(contents, '\r?\n'); // contents.split('\n');
        } catch (System.ListException e) {
            System.debug('Limits exceeded?  ' + e.getMessage());
        }
        Integer num = 0;
        for (String line : lines) {
            // check for blank CSV lines (only commas)
            if (line.replaceAll(',', '').trim().length() == 0)
                break;
            List<String> fields = line.split(',');
            List<String> cleanFields = new List<String>();
            String compositeField;
            Boolean makeCompositeField = false;
            for (String field : fields) {
                if (field.startsWith('"') && field.endsWith('"')) {
                    cleanFields.add(field.replaceAll('DBLQT', '"').removeStart('"').removeEnd('"'));
                } else if (field.startsWith('"')) {
                    makeCompositeField = true;
                    compositeField = field;
                } else if (field.endsWith('"')) {
                    compositeField += ',' + field;
                    cleanFields.add(compositeField.replaceAll('DBLQT', '"').removeStart('"').removeEnd('"'));
                    makeCompositeField = false;
                } else if (makeCompositeField) {
                    compositeField += ',' + field;
                } else {
                    cleanFields.add(field.replaceAll('DBLQT', '"').removeStart('"').removeEnd('"'));
                }
            }
            allFields.add(cleanFields);
        }
        if (skipHeaders) {
            allFields.remove(0);
        }
        return allFields;
    }

    /**
     * Split a string of any size, while avoiding the dreaded 'Regex too complicated'
     * error, which the String.split(String) method causes on some large inputs.
     *
     * Note that this method does not avoid other errors, such as those related to
     * excess heap size or CPU time.
     */
    public static List<String> safeSplit(String inStr, String delim) {
        Integer regexFindLimit = 100;
        Integer regexFindCount = 0;

        List<String> output = new List<String>();

        Matcher m = Pattern.compile(delim).matcher(inStr);

        Integer lastEnd = 0;

        while (!m.hitEnd()) {
            while (regexFindCount < regexFindLimit && !m.hitEnd()) {
                if (m.find()) {
                    output.add(inStr.substring(lastEnd, m.start()));
                    lastEnd = m.end();
                } else {
                    output.add(inStr.substring(lastEnd));
                    lastEnd = inStr.length();
                }

                regexFindCount++;
            }

            // Note: Using region() to advance instead of substring() saves
            // drastically on heap size. Nonetheless, we still must reset the
            // (unmodified) input sequence to avoid a 'Regex too complicated'
            // error.
            m.reset(inStr);
            m.region(lastEnd, m.regionEnd());

            regexFindCount = 0;
        }

        return output;
    }

    /* @description Retrieves the field value from a SObject and escapes characters
     *  so they can be used in a CSV file
     * @param   SObject     record to get the values from
     * @param   String      name of the field whose value should be retrieved
     * @return  String      String value of that record.Field__c
     */
    public static String readFieldValue(SObject record, String fieldName) {
        List<String> relationships = fieldName.split('\\.');
        String fieldValue;
        System.debug(relationships);
        if (relationships.size() > 1) {
            SObject parent = record;
            for (Integer i = 0; i < relationships.size() - 1; i++) {
                parent = parent.getSObject(relationships[i]);
                if (parent == null) {
                    return '';
                }
            }
            fieldValue = String.valueOf(parent.get(relationships.get(relationships.size() - 1)));
        } else {
            fieldValue = String.valueOf(record.get(fieldName));
        }

        if (String.isBlank(fieldValue)) {
            return '';
        }

        if (fieldName.equals('Birthday__c') || fieldName.endsWith('Birthdate__c')) {
            Date dateValue = Date.valueOf(fieldValue);
            fieldValue = String.valueOf(
                Datetime.newInstance(dateValue.year(), dateValue.month(), dateValue.day()).format('MM/dd/yyyy')
            );
        }

        if (fieldValue.contains(',') || fieldValue.contains('\n') || fieldValue.contains('"')) {
            fieldValue = '"' + fieldValue.replace('"', '\'') + '"';
        }

        return fieldValue;
    }

    /* @description Validates a list of Emails using SF Regex
     * @param   List<String>    List of emails to validate
     * @return  Map<String, Boolean>    email - valid map
     */
    public static Map<String, Boolean> validateEmail(List<String> emails) {
        Map<String, Boolean> result = new Map<String, Boolean>();
        Pattern emailPattern = Pattern.compile(EMAIL_REGEX);
        for (String email : emails) {
            Boolean isValid = false;
            if (!String.isBlank(email)) {
                Matcher emailMatcher = emailPattern.matcher(email);
                isValid = emailMatcher.matches();
            }
            result.put(email, isValid);
        }
        return result;
    }

    /* @description Validates an email using SF Regex
     * @param   String  email to validate
     * @return  Map<String, Boolean>    email - valid map
     */
    public static Boolean validateEmail(String email) {
        if (String.isBlank(email)) {
            return false;
        } else {
            Pattern emailPattern = Pattern.compile(EMAIL_REGEX);
            Matcher emailMatcher = emailPattern.matcher(email);
            return emailMatcher.matches();
        }
    }
}
