/**
 * xit-validation.js
 * 
 * 기본적인 입력값 검증을 위한 유틸리티 함수들을 제공합니다.
 * 
 * @author 개발자
 * @since 2023-06-01
 */

// 유효성 검사 실패 시 스타일 정의
const style = document.createElement('style');
style.textContent = `
    .is-invalid {
        border: 1px solid #e57373 !important;
        box-shadow: 0 0 0 0.1rem rgba(229, 115, 115, 0.15) !important;
    }
    .is-invalid-custom {
        border: 1px solid #e57373 !important;
        box-shadow: 0 0 0 0.1rem rgba(229, 115, 115, 0.15) !important;
    }
    .error-message {
        color: #e57373;
        font-size: 0.875rem;
        margin-top: 0.25rem;
    }
`;
document.head.appendChild(style);

/**
 * 한글 문자 바이트 계산 설정
 * 한글 문자를 몇 바이트로 계산할지 설정합니다.
 */
var KOREAN_CHAR_BYTE_LENGTH = 3; // 한글 한 글자를 3바이트로 계산


/**
 * validation-check 속성을 가진 모든 폼 필드를 검증합니다.
 *
 * @param {string} formId - 검증할 폼의 ID
 * @returns {boolean} - 모든 검증이 성공하면 true, 하나라도 실패하면 false
 */
function validateFormByAttributes(formId) {
    const form = document.getElementById(formId);
    if (!form) {
        console.error(`폼 ID '${formId}'를 찾을 수 없습니다.`);
        return false;
    }

    // validation-check 속성을 가진 모든 요소 선택
    const elements = form.querySelectorAll('[validation-check]');
    let isValid = true;
    let focusTarget = null;

    // 각 요소에 대해 검증 수행
    elements.forEach(element => {
        const elementValid = validateByAttribute(element);
        isValid = isValid && elementValid;
        if( !elementValid ){
            focusTarget = element;
        }
    });

    // 별도 함수로 진행된 validation 중에 is-invalid class 가 포함되어 있으면 취소
    elements.forEach(element => {
        if( element.classList.contains('is-invalid-custom') ){
            isValid = false;
            focusTarget = element;
        }
    });
    if( focusTarget ){
        focusTarget.focus();
    }

    return isValid;
}

/**
 * validation-check 속성을 기반으로 폼 필드를 검증합니다.
 * 모든 입력 태그(input, radio, select 등)에 적용 가능합니다.
 *
 * @param {HTMLElement} element - 검증할 HTML 요소
 * @returns {boolean} - 검증 성공 시 true, 실패 시 false
 */
function validateByAttribute(element) {
    if (!element) {
        console.error('검증할 요소가 제공되지 않았습니다.');
        return false;
    }

    const validationType = element.getAttribute('validation-check');
    if (   element.getAttribute('disabled')
        || element.getAttribute('disabled') === 'disabled'
        || element.getAttribute('disabled') === 'true'
       ) {
        // validation-check 속성이 없고, disabled 값은 검증 성공으로 간주
        return true;
    }

    // 태그 유형에 따라 값을 가져오는 방식을 다르게 구현
    let value = '';
    const tagName = element.tagName.toUpperCase();
    const id = element.id;

    if (tagName === 'INPUT') {
        const inputType = element.type.toLowerCase();

        if (inputType === 'radio') {
            // 라디오 버튼의 경우 같은 name을 가진 모든 요소 중 checked 속성이 있는 요소의 value를 가져옴
            const name = element.name;
            const radioGroup = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
            const checkedRadio = Array.from(radioGroup).find(radio => radio.checked);
            value = checkedRadio ? checkedRadio.value : '';
        } else if (inputType === 'checkbox') {
            // 체크박스의 경우 checked 속성을 확인
            value = element.checked ? element.value : '';
        } else {
            // 기타 input 태그(text, email, number 등)
            value = element.value;
        }
    } else if (tagName === 'SELECT') {
        // select 태그의 경우 selectedIndex와 value 속성을 확인
        value = element.options[element.selectedIndex]?.value || '';
    } else {
        // 기타 태그
        value = element.value || '';
    }

    let isValid = true;
    let errorMessage = '';

    // validation-check 속성 값에 따라 검증 수행
    const validationTypes = validationType.split(' ');

    for (let i = 0; i < validationTypes.length; i++) {
        const type = validationTypes[i];

        switch (type) {
            case 'required':
                if (isEmpty(value)) {
                    isValid = false;
                    errorMessage = '필수 입력 항목입니다.';
                }
                break;

            case 'email':
                if (!isEmpty(value) && !isEmail(value)) {
                    isValid = false;
                    errorMessage = '유효한 이메일 주소를 입력해주세요.';
                }
                break;

            case 'phone':
                if (!isEmpty(value) && !isPhoneNumber(value)) {
                    isValid = false;
                    errorMessage = '유효한 전화번호를 입력해주세요.';
                }
                break;

            case 'number':
                if (!isEmpty(value) && !isNumber(value)) {
                    isValid = false;
                    errorMessage = '숫자만 입력해주세요.';
                }
                break;

            case 'integer':
                if (!isEmpty(value) && !isInteger(value)) {
                    isValid = false;
                    errorMessage = '정수만 입력해주세요.';
                }
                break;

            case 'korean':
                if (!isEmpty(value) && !isKorean(value)) {
                    isValid = false;
                    errorMessage = '한글만 입력해주세요.';
                }
                break;

            case 'alpha':
                if (!isEmpty(value) && !isAlpha(value)) {
                    isValid = false;
                    errorMessage = '영문자만 입력해주세요.';
                }
                break;

            case 'alphanumeric':
                if (!isEmpty(value) && !isAlphaNumeric(value)) {
                    isValid = false;
                    errorMessage = '영문자와 숫자만 입력해주세요.';
                }
                break;

            case 'date':
                if (!isEmpty(value) && !isDate(value)) {
                    isValid = false;
                    errorMessage = '유효한 날짜 형식(YYYY-MM-DD)으로 입력해주세요.';
                }
                break;

            case 'url':
                if (!isEmpty(value) && !isUrl(value)) {
                    isValid = false;
                    errorMessage = '유효한 URL을 입력해주세요.';
                }
                break;

            default:
                // 지원하지 않는 validation 타입은 무시
                console.debug(`type이 없거나 지원하지 않는 validation 타입: ${type}`);
                break;

            /*
            default:
                if (type.startsWith('otherFunction:')) {
                    const fnName = type.split(':')[1];

                    if (typeof customFunctions[fnName] === 'function') {
                        const result = customFunctions[fnName](value);
                        if (!result.valid) {
                            isValid = false;
                            errorMessage = result.message;
                        }
                    } else {
                        console.warn(`정의되지 않은 otherFunction: ${fnName}`);
                    }
                } else {
                    console.warn(`지원하지 않는 validation 타입: ${type}`);
                }
                break;
             */
        }

        // 검증 실패 시 루프 종료
        if (!isValid) {
            break;
        }
    }

    // maxlength 속성 체크 (validation-check 속성 체크 이후에 수행)
    const maxlength = element.getAttribute('maxlength');
    if (isValid && maxlength && !isEmpty(value)) {
        const byteLength = getByteLength(value);
        if (byteLength > parseInt(maxlength, 10)) {
            isValid = false;
            errorMessage = `입력 가능한 최대 길이를 초과했습니다. (현재: ${byteLength}바이트, 최대: ${maxlength}바이트)`;
        }
    }

    // min, max 속성 체크 (validation-check 속성 체크 이후에 수행)
    const max = element.getAttribute('max');
    const min = element.getAttribute('min');
    if (isValid && max && min && !isEmpty(value)) {
        if (Number(value) > Number(max) || Number(value) < Number(min)) {
            isValid = false;
            errorMessage = `입력 가능한 숫자를 초과했습니다. (현재: ${value}, 최소: ${min}, 최대: ${max})`;
        }
    }

    // 검증 결과에 따라 UI 업데이트
    const errorElement = document.getElementById(`${id}-error`);

    if (!isValid) {
        element.classList.add('is-invalid');
        if (errorElement) {
            errorElement.textContent = errorMessage;
            errorElement.style.display = 'block';
        } else {
            // 오류 메시지 요소가 없으면 생성
            const newErrorElement = document.createElement('div');
            newErrorElement.id = `${id}-error`;
            newErrorElement.className = 'error-message';
            newErrorElement.style.display = 'block';
            newErrorElement.style.color = 'red';
            newErrorElement.textContent = errorMessage;

            // 중요로직: 해당 객체의 parent 끝에 에러 메시지 추가
            //element.parentNode.insertBefore(newErrorElement, element.nextSibling);
            element.parentNode.appendChild(newErrorElement);
        }
    } else {
        element.classList.remove('is-invalid');
        if (errorElement) {
            errorElement.textContent = '';
            errorElement.style.display = 'none';
        }
    }

    return isValid;
}


function errorElementCreate(element, errorMessage, isValid) {
    const id = element.id;
    const errorElement = document.getElementById(`${id}-error-custom`);

    if (!isValid) {
        element.classList.add('is-invalid-custom');
        if (errorElement) {
            errorElement.textContent = errorMessage;
            errorElement.style.display = 'block';
        } else {
            // 오류 메시지 요소가 없으면 생성
            const newErrorElement = document.createElement('div');
            newErrorElement.id = `${id}-error-custom`;
            newErrorElement.className = 'error-message';
            newErrorElement.style.display = 'block';
            newErrorElement.style.color = 'red';
            newErrorElement.textContent = errorMessage;

            // 중요로직: 해당 객체의 parent 끝에 에러 메시지 추가
            //element.parentNode.insertBefore(newErrorElement, element.nextSibling);
            element.parentNode.appendChild(newErrorElement);
        }
    } else {
        element.classList.remove('is-invalid-custom');
        if (errorElement) {
            errorElement.textContent = '';
            errorElement.style.display = 'none';
        }
    }
}



/**
 * 문자열의 바이트 길이를 계산합니다.
 * 한글은 KOREAN_CHAR_BYTE_LENGTH 바이트, 그 외 문자는 1바이트로 계산합니다.
 * 
 * @param {string} str - 바이트 길이를 계산할 문자열
 * @returns {number} - 문자열의 바이트 길이
 */
function getByteLength(str) {
    if (isEmpty(str)) {
        return 0;
    }

    var byteLength = 0;
    for (var i = 0; i < str.length; i++) {
        var charCode = str.charCodeAt(i);
        // 한글 유니코드 범위: AC00-D7A3 (가-힣)
        if (charCode >= 0xAC00 && charCode <= 0xD7A3) {
            byteLength += KOREAN_CHAR_BYTE_LENGTH;
        } 
        // 한글 자음/모음 유니코드 범위: 3131-318E (ㄱ-ㅎ, ㅏ-ㅣ)
        else if (charCode >= 0x3131 && charCode <= 0x318E) {
            byteLength += KOREAN_CHAR_BYTE_LENGTH;
        }
        else {
            byteLength += 1;
        }
    }
    return byteLength;
}

/**
 * 값이 비어있는지 확인합니다., common_util.js 에 있는거 사용
 * 
 * @param {*} value - 검증할 값
 * @returns {boolean} - 값이 비어있으면 true, 그렇지 않으면 false
 */
function isEmpty(value) {
    if (value === null || value === undefined) {
        return true;
    }

    if (typeof value === 'string' && value.trim() === '') {
        return true;
    }

    if (Array.isArray(value) && value.length === 0) {
        return true;
    }

    if (typeof value === 'object' && Object.keys(value).length === 0) {
        return true;
    }
    return false;
}

/**
 * 값이 최소 길이 이상인지 확인합니다.
 * 
 * @param {string} value - 검증할 문자열
 * @param {number} minLength - 최소 길이
 * @returns {boolean} - 값이 최소 길이 이상이면 true, 그렇지 않으면 false
 */
function isMinLength(value, minLength) {
    if (isEmpty(value)) {
        return false;
    }

    return value.length >= minLength;
}

/**
 * 값이 최대 길이 이하인지 확인합니다.
 * 
 * @param {string} value - 검증할 문자열
 * @param {number} maxLength - 최대 길이
 * @returns {boolean} - 값이 최대 길이 이하이면 true, 그렇지 않으면 false
 */
function isMaxLength(value, maxLength) {
    if (isEmpty(value)) {
        return true;
    }

    return value.length <= maxLength;
}

/**
 * 값이 숫자인지 확인합니다.
 * 
 * @param {*} value - 검증할 값
 * @returns {boolean} - 값이 숫자이면 true, 그렇지 않으면 false
 */
function isNumber(value) {
    if (isEmpty(value)) {
        return false;
    }

    return !isNaN(value) && isFinite(value);
}

/**
 * 값이 정수인지 확인합니다.
 * 
 * @param {*} value - 검증할 값
 * @returns {boolean} - 값이 정수이면 true, 그렇지 않으면 false
 */
function isInteger(value) {
    if (!isNumber(value)) {
        return false;
    }

    return Number.isInteger(Number(value));
}

/**
 * 값이 이메일 형식인지 확인합니다.
 * 
 * @param {string} value - 검증할 이메일 주소
 * @returns {boolean} - 값이 이메일 형식이면 true, 그렇지 않으면 false
 */
function isEmail(value) {
    if (isEmpty(value)) {
        return false;
    }

    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return emailRegex.test(value);
}

/**
 * 값이 전화번호 형식인지 확인합니다.
 * 
 * @param {string} value - 검증할 전화번호
 * @returns {boolean} - 값이 전화번호 형식이면 true, 그렇지 않으면 false
 */
function isPhoneNumber(value) {
    if (isEmpty(value)) {
        return false;
    }

    // 하이픈(-) 포함 또는 미포함 전화번호 형식 검증
    const phoneRegex = /^(01[016789]|02|0[3-9][0-9])-?[0-9]{3,4}-?[0-9]{4}$/;
    return phoneRegex.test(value);
}

/**
 * 값이 날짜 형식인지 확인합니다. (YYYY-MM-DD)
 * 
 * @param {string} value - 검증할 날짜
 * @returns {boolean} - 값이 날짜 형식이면 true, 그렇지 않으면 false
 */
function isDate(value) {
    if (isEmpty(value)) {
        return false;
    }

    // YYYY-MM-DD 형식 검증
    const dateRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
    if (!dateRegex.test(value)) {
        return false;
    }

    // 실제 유효한 날짜인지 확인
    const parts = value.split('-');
    const year = parseInt(parts[0], 10);
    const month = parseInt(parts[1], 10) - 1; // 월은 0부터 시작
    const day = parseInt(parts[2], 10);

    const date = new Date(year, month, day);
    return date.getFullYear() === year && 
           date.getMonth() === month && 
           date.getDate() === day;
}

/**
 * 값이 URL 형식인지 확인합니다.
 * 
 * @param {string} value - 검증할 URL
 * @returns {boolean} - 값이 URL 형식이면 true, 그렇지 않으면 false
 */
function isUrl(value) {
    if (isEmpty(value)) {
        return false;
    }

    // 절대 URL 패턴 (http://, https://, ftp:// 등으로 시작)
    const absoluteUrlPattern = /^(?:(?:https?|ftp):\/\/)/i;

    // 상대 URL 패턴 (슬래시로 시작하거나 영문자로 시작하는 경로)
    const relativeUrlPattern = /^\/[\w\-\.~:\/\?#\[\]@!\$&'\(\)\*\+,;=]+$|^[\w\-\.]+\/[\w\-\.~:\/\?#\[\]@!\$&'\(\)\*\+,;=]*$/;

    // 절대 URL인 경우 URL 생성자로 검증
    if (absoluteUrlPattern.test(value)) {
        try {
            new URL(value);
            return true;
        } catch (e) {
            return false;
        }
    }

    // 상대 URL인 경우 패턴 매칭으로 검증
    return relativeUrlPattern.test(value);
}

/**
 * 값이 알파벳만 포함하는지 확인합니다.
 * 
 * @param {string} value - 검증할 문자열
 * @returns {boolean} - 값이 알파벳만 포함하면 true, 그렇지 않으면 false
 */
function isAlpha(value) {
    if (isEmpty(value)) {
        return false;
    }

    const alphaRegex = /^[a-zA-Z]+$/;
    return alphaRegex.test(value);
}

/**
 * 값이 알파벳과 숫자만 포함하는지 확인합니다.
 * 
 * @param {string} value - 검증할 문자열
 * @returns {boolean} - 값이 알파벳과 숫자만 포함하면 true, 그렇지 않으면 false
 */
function isAlphaNumeric(value) {
    if (isEmpty(value)) {
        return false;
    }

    const alphaNumericRegex = /^[a-zA-Z0-9]+$/;
    return alphaNumericRegex.test(value);
}

/**
 * 값이 한글만 포함하는지 확인합니다.
 * 
 * @param {string} value - 검증할 문자열
 * @returns {boolean} - 값이 한글만 포함하면 true, 그렇지 않으면 false
 */
function isKorean(value) {
    if (isEmpty(value)) {
        return false;
    }

    const koreanRegex = /^[가-힣]+$/;
    return koreanRegex.test(value);
}

/**
 * 폼 필드를 검증합니다.
 * 
 * @param {string} fieldId - 검증할 필드의 ID
 * @param {Function} validationFn - 검증 함수
 * @param {string} errorMessage - 검증 실패 시 표시할 오류 메시지
 * @returns {boolean} - 검증 성공 시 true, 실패 시 false
 */
function validateField(fieldId, validationFn, errorMessage) {
    const field = document.getElementById(fieldId);
    if (!field) {
        console.error(`필드 ID '${fieldId}'를 찾을 수 없습니다.`);
        return false;
    }

    const errorElement = document.getElementById(`${fieldId}-error`);
    const value = field.value;

    if (!validationFn(value)) {
        field.classList.add('is-invalid');
        if (errorElement) {
            errorElement.textContent = errorMessage;
            errorElement.style.display = 'block';
        } else {
            console.warn(`오류 메시지 요소 '${fieldId}-error'를 찾을 수 없습니다.`);
        }
        return false;
    }

    field.classList.remove('is-invalid');
    field.classList.add('is-valid');
    if (errorElement) {
        errorElement.textContent = '';
        errorElement.style.display = 'none';
    }

    return true;
}

/**
 * 폼 전체를 검증합니다.
 * 
 * @param {Array} validations - 검증 설정 배열 [{fieldId, validationFn, errorMessage}, ...]
 * @returns {boolean} - 모든 검증이 성공하면 true, 하나라도 실패하면 false
 */
function validateForm(validations) {
    let isValid = true;

    validations.forEach(validation => {
        const { fieldId, validationFn, errorMessage } = validation;
        const fieldValid = validateField(fieldId, validationFn, errorMessage);
        isValid = isValid && fieldValid;
    });

    return isValid;
}

/**
 * 필수 입력 필드를 검증합니다.
 * 
 * @param {string} fieldId - 검증할 필드의 ID
 * @param {string} fieldName - 필드 이름 (오류 메시지에 표시)
 * @returns {boolean} - 검증 성공 시 true, 실패 시 false
 */
function validateRequired(fieldId, fieldName) {
    return validateField(
        fieldId,
        value => !isEmpty(value),
        `${fieldName}은(는) 필수 입력 항목입니다.`
    );
}

/**
 * 이메일 필드를 검증합니다.
 * 
 * @param {string} fieldId - 검증할 필드의 ID
 * @returns {boolean} - 검증 성공 시 true, 실패 시 false
 */
function validateEmail(fieldId) {
    return validateField(
        fieldId,
        value => isEmpty(value) || isEmail(value),
        '유효한 이메일 주소를 입력해주세요.'
    );
}

/**
 * 전화번호 필드를 검증합니다.
 * 
 * @param {string} fieldId - 검증할 필드의 ID
 * @returns {boolean} - 검증 성공 시 true, 실패 시 false
 */
function validatePhoneNumber(fieldId) {
    return validateField(
        fieldId,
        value => isEmpty(value) || isPhoneNumber(value),
        '유효한 전화번호를 입력해주세요.'
    );
}