const jsonPath = require('jsonpath');
const XLSX = require('xlsx');
const psd = require('psd');
const fs = require('fs');
const path = require('path');
const xpath = require('xpath');
const { DOMParser } = require('xmldom');

const findSimilarString = require('./findSimilarString');
const getGpdpScore = require('./gpdpScoring.js');
const getToday = require('./getToday.js');
const todayDate = getToday();

const examRound = '2507';

const DICorDPI = 'DIC'
// const DICorDPI = 'DPI'
const examTypes = [
  // 'A',
  'B',
  // 'C',
  // 'D'
];

// testMode가 true일 경우 TEST 폴더에 있는 답안 파일을 읽어옴
const testMode = false;
// const testMode = true;

const outputExcelFiles = [];

examTypes.forEach(type => {
  const jsonPath = `./${DICorDPI}_${examRound}${type}.json`
  if (!fs.existsSync(jsonPath)) return;
  const scoringJson = require(jsonPath);
  const answerFilesDir = `./output/${examRound}/${type}/${testMode ? 'TEST' : DICorDPI}`;
  let outputExcelFile = `./${todayDate}_${DICorDPI}_${examRound}${type}_채점결과.xlsx`;
  if (testMode) {
    outputExcelFile = `./00_${DICorDPI}_${examRound}${type}_TEST.xlsx`;
  }

  // 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
  const studentDirs = fs.readdirSync(answerFilesDir).filter(file => {
    const filePath = path.join(answerFilesDir, file);
    return fs.statSync(filePath).isDirectory();
  });

  // 채점 결과 리스트
  const scoringResultList = [];
  const psdData = [];

  studentDirs.forEach(student => {
    // 맥에서 한글 디렉토리 이름을 읽어서 엑셀에 저장 할 시 자소 분리가 되어 저장되는 문제 노말라이즈해서 해결
    const name = student.normalize('NFC');
    const studentDir = path.join(answerFilesDir, student);
    // const psdFiles = fs.readdirSync(studentDir).filter(file => file.endsWith('.psd'));
    const psdFiles = fs.readdirSync(studentDir).filter(file => file.toLowerCase().endsWith('.psd'));

    // DIAT시험 프로젝트로 생성시 gmep확장자로
    // 교육용 프로젝프로 생성시 gmdp확장자로 생성됨
    const gmepFile = fs.readdirSync(studentDir).filter(
      file => file.toLowerCase().endsWith('.gmep')
      // || file.toLowerCase().endsWith('.gmdp')  
    );

    // 곰픽 파일 gpdp 파일 이거나 xml 파일
    const gpdpFiles = fs.readdirSync(studentDir).filter(
      file => file.toLowerCase().endsWith('.xml')
        || (file === null && (name + '.xml'))
    );

    // 학생 이름을 key로 하는 객체 생성
    // 채점결과
    const scoringResult = {
      0: name
    };

    psdFiles.forEach((psdFile, index) => {
      const psdPath = path.join('./', studentDir, psdFile);

      console.log('');
      console.log(`➡️ Reading ${psdPath}...`);
      try {
        const psdFileData = psd.fromFile(psdPath);
        psdFileData.parse();
        psdData[index] = psdFileData;
        scoringResult[index + 1] = getScore(psdData, scoringJson, index);
      } catch (error) {
        console.error(`Error reading PSD file: ${psdPath}`, error);
      }
    });
    gpdpFiles.forEach((gpdpFile, index) => {
      const gpdpPath = path.join('./', studentDir, gpdpFile);
      console.log(`Reading ${gpdpPath}...`);

      const xmlString = fs.readFileSync(gpdpPath, 'utf8');
      // XML 문자열을 파싱하여 XML 문서 객체로 변환
      const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml');
      // console.log('xmlDocument:', xmlDocument);

      scoringResult[index + 1] = getGpdpScore(xmlDocument, scoringJson, index + 4);
    });
    if (gmepFile.length === 0) {
      // 곰믹스 채점 항목 갯수
      const gmepItemCount = Object.keys(scoringJson[2]).length - 2;
      // console.log("🚀 ~ gmepItemCount:", gmepItemCount)

      scoringResult[3] = {};
      for (let i = 1; i <= gmepItemCount; i++) {
        scoringResult[3][i] = 0;
      }
      scoringResult[3]['총점'] = 0;
    }
    else {
      gmepFile.forEach((gmep, index) => {
        const gmepPath = path.join('./', studentDir, gmep);

        console.log('');
        console.log(`➡️ Reading ${gmepPath}...`);

        const xmlString = fs.readFileSync(gmepPath, 'utf8');
        // XML 문자열을 파싱하여 XML 문서 객체로 변환
        const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml');
        // console.log('xmlDocument:', xmlDocument);

        scoringResult[3] = getGmepScore(xmlDocument, scoringJson, 2);
      });
    }
    scoringResultList.push(scoringResult);
  });

  const flattenedData = prepareExcelData(scoringResultList);
  const transposedData = transposeData(flattenedData);

  // 엑셀 파일 생성
  const worksheet = XLSX.utils.json_to_sheet(transposedData, { skipHeader: true });
  const workbook = XLSX.utils.book_new();

  // 열 너비 계산
  const columnWidths = Object.keys(transposedData[0]).map(key => {
    // 각 열의 최대 길이를 계산
    const maxLength = Math.max(
      // key.length, // 열 제목의 길이
      // ...transposedData.map(row => (row[key] ? row[key].toString().length : 0)) // 각 셀의 데이터 길이
      4 // 고정 너비
    );
    return { wch: maxLength + 1 }; // 여유 공간 추가
  });

  // 열 너비 설정
  worksheet['!cols'] = columnWidths;
  // Add the worksheet to the workbook
  XLSX.utils.book_append_sheet(workbook, worksheet, '채점 결과');

  // 엑셀 파일 저장
  XLSX.writeFile(workbook, outputExcelFile);
  outputExcelFiles.push(outputExcelFile);
});

console.log('채점 결과');
outputExcelFiles.forEach((outputFile, index) => {
  console.log(`[${index + 1}] : ${outputFile}`);
});


// xml 형식의 gmep 파일을 읽어서 점수를 계산
// scoring.json 파일 내에 있는 ele 요소는 xpath 형식으로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산
// scoring.json 파일 내에 있는 type은 비교할 값의 타입을 의미하며, boolean, array 등이 있음
// scoring.json 파일 내에 있는 type에 따라 비교하는 방식이 달라짐
// 채점 결과를 scoringResultList 배열에 저장
function getGmepScore(gmepData, scoringJson, index) {
  function compareAndScore(user, right, point, key, scoringResult, options = {}) {
    let score = 0;
    let isEqual = false;

    const {
      tolerance = 0,  // 숫자 비교 시 허용 오차
      type = 'auto',  // 'auto' | 'number[]' | 'string[]' | 'object' | 'primitive'
    } = options;

    if (type === 'force-correct') {
      isEqual = true;
    }
    else if (Array.isArray(right) && Array.isArray(user)) {
      if (right.length === user.length) {
        if
          (type === 'number[]' ||
          (type === 'auto' && typeof right[0] === 'number')) {
          isEqual = right.every((val, idx) => Math.abs(val - user[idx]) <= tolerance);
        }
        else if
          (type === 'string[]' ||
          (type === 'auto' && typeof right[0] === 'string')) {
          isEqual = right.every((val, idx) => val === user[idx]);
        }
      }
    }
    else if
      (type === 'object' ||
      (type === 'auto' && typeof right === 'object' && typeof user === 'object')) {
      isEqual = JSON.stringify(user) === JSON.stringify(right);
    }
    else {
      isEqual = user == right;  // primitive 비교
    }

    if (isEqual) {
      score = point;
      console.log('작성답안: ', user);
      console.log('>⭕ 정답: ', right);
    } else {
      console.log('작성답안: ', user);
      console.log('>❌ 오답: ', right);
    }

    scoringResult[key] = score;
    // totalScore += score;
    return score;
  }

  function convertColorToHex(colorValue) {
    // 문자열이면 정수로 변환
    const intValue = typeof colorValue === 'string' ? parseInt(colorValue, 10) : colorValue;

    // 부호 없는 32비트 정수로 변환 → 8자리 16진수 문자열로
    const hex = (intValue >>> 0).toString(16).padStart(8, '0');

    // 하위 6자리 추출 (BGR 순서)
    const bgr = hex.slice(2);  // 예: "fff1b01d" → "f1b01d"

    // BGR → RGB로 재배열
    const b = bgr.slice(0, 2);
    const g = bgr.slice(2, 4);
    const r = bgr.slice(4, 6);

    // RGB 순서로 합치고 소문자로 반환
    return (r + g + b).toLowerCase();  // e.g., "fd5721"
  }

  const gmepXmlDoc = gmepData;
  const scoringResult = {};

  const scoringData = scoringJson[index];
  // console.log(scoringData);

  // 동영상 시작시간, 오프닝 시작시간 상수값 저장
  const videoStartTime = scoringData['10'].value;
  const openingStartTime = scoringData['28'].value;

  let totalScore = 0;

  // 채점기준표 문항별 분류
  for (const key in scoringData) {

    // XML <CRClip> 요소의 index값을 구함
    function getCRClipIndex(mediaName) {
      // CRClipArr/CRClip 요소의 Path속성 리스트를 구함
      // 모션 클립 이미지도 고려해 처리
      const mediaPathList = xpath.select("//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path", gmepXmlDoc);

      // "동영상.mp4"의 clipIndex를 구함
      // clipIndex가 -1이면 해당 미디어가 존재하지 않는 것
      const crclipIndex = mediaPathList.findIndex(mediaPath => mediaPath.value === mediaName);
      return crclipIndex;
    }

    // 자막 텍스트로 자막클립인덱스 반환
    function getClipIndexByText(text) {
      const crOwneUnits = xpath.select(`//CROwneUnitArr/CROwneUnit`, gmepXmlDoc);

      let subtitleClipIndex = null;
      // 자막 텍스트와 일치하는 요소의 인덱스를 반환
      for (let i = 0; i < crOwneUnits.length; i++) {
        const crcUnitArr = xpath.select1('.//CRCUnitArr', crOwneUnits[i]);
        if (crcUnitArr && crcUnitArr.getAttribute('Name') === text) {
          subtitleClipIndex = i;
          break;
        }
      }
      // console.log('🟢 자막 텍스트로 검색한 CROwneUnit 인덱스 : ', subtitleClipIndex);
      return subtitleClipIndex;
    }

    function getClipIndexByOrder(order) {
      // 자막의 갯수가 2개 이상 (오프닝과 동영상 자막이 있을 경우)
      // 앞은 오프닝 뒤는 동영상 자막으로 판단
      // crTrackClips[0] : 오프닝 자막
      // crTrackClips[1] : 동영상 자막  
      const crOwneUnits = xpath.select(`//CROwneUnitArr/CROwneUnit`, gmepXmlDoc);
      const crTrackClips = xpath.select("//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@Type='0') and not(@ClipIndex='-1')]", gmepXmlDoc);
      let subtitleClipIndex = null;
      if (subtitleClipIndex === null && crOwneUnits.length >= 2) {
        // if (crOwneUnits.length >= 2) {
        for (let i = 0; i < crTrackClips.length; i++) {
          if ((order - 1) === i) {
            subtitleClipIndex = parseInt(crTrackClips[i].getAttribute('ClipIndex'), 10);
            break;
          }
        }
      }
      // console.log('🟡 자막 순서로 검색한 CROwneUnit 인덱스 : ', subtitleClipIndex);
      return subtitleClipIndex;
    }

    // 자막들의 시작 시간을 가지는 리스트에서
    // 구하고자 하는 영상의 시작시간(startTime)과 일치하는 시간이 있다면 인덱스를 가져와
    // 자막의 인덱스를 구함
    function getCilpIndexByStartTime(startTime) {
      const crTrackClips = xpath.select("//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip", gmepXmlDoc);
      const subtitleStartTimeList = getStartTimeList('텍스트');
      const startTimeIndex = subtitleStartTimeList.findIndex(value => value === startTime);

      let subtitleClipIndex = null;
      for (let i = 0; i < crTrackClips.length; i++) {
        if (parseInt(crTrackClips[i].getAttribute('ClipIndex'), 10) == -1) {
          continue;
        }
        const clipIndex = parseInt(crTrackClips[i].getAttribute('ClipIndex'), 10);
        if (startTimeIndex === i) {
          subtitleClipIndex = clipIndex;
          break;
        }
      }
      // console.log('🟠 자막 시작시간으로 검색한 CROwneUnit 인덱스 : ', subtitleClipIndex);
      return subtitleClipIndex;
    }

    // 영상내 존재하는 자막과 자막사이 공백의 시작시간 리스트를 구함
    function getStartTimeList(type) {
      let trackClips = null;

      if (type === '텍스트') {
        trackClips = xpath.select(`//CRTrackList[@Name='${type}' or @Name='비디오2']/CRTrackClip`, gmepXmlDoc);
      }
      else if (type === '비디오1' || type === '오디오1') {
        trackClips = xpath.select(`//CRTrackList[@Name='${type}']/CRTrackClip`, gmepXmlDoc);
      }

      let cumulativeLengths = [];
      let total = 0;

      for (let i = 0; i < trackClips.length; i++) {
        const length = parseInt(trackClips[i].getAttribute('Length'), 10);

        cumulativeLengths.push(total);
        total += length;
      }

      // console.log("🔵 자막 구간 시작시간 : ", cumulativeLengths);
      return cumulativeLengths;
    }

    function getCRTrackClipIndex(crclipIndex, type) {
      let xpathQuery;
      if (type === '텍스트') {
        xpathQuery = `//CRTrackList[@Name='${type}' or @Name='비디오2']/CRTrackClip`;
      }
      else if (type === '비디오1' || type === '오디오1') {
        xpathQuery = `//CRTrackList[@Name='${type}']/CRTrackClip`;
      }
      else {
        throw new Error(`Unknown type: ${type}`);
      }

      const crTrackClips = xpath.select(xpathQuery, gmepXmlDoc);

      let index = -1;
      // crTrackClips에서 ClipIndex가 crclipIndex와 일치하는 요소의 인덱스를 찾음
      // 처음 발견하는 요소의 인덱스를 반환
      for (let i = 0; i < crTrackClips.length; i++) {
        if (crclipIndex == parseInt(crTrackClips[i].getAttribute('ClipIndex'), 10)) {
          index = i;
          break;
        }
      }

      return index;
    }

    let ele = scoringData[key].ele;
    const rightAnswer = scoringData[key].value;
    const point = scoringData[key].point;
    const type = scoringData[key].type ? scoringData[key].type : '';
    let search = scoringData[key].search;
    const media = scoringData[key].media;

    // const videoStartTime = scoringData.videoStartTime;
    // const openingStartTime = scoringData.openingStartTime;

    const image = scoringData[key].image;

    let userAnswer = null;

    console.log(`example number: ${key}`)

    const subtitleOrder = type.includes('opening') ? 1 : type.includes('video') ? 2 : null;

    // search 값이 undefined 아니면 ele의 {search}부분을 search로 치환
    /**
     * JSON파일 곰믹스 5번문항/22번 문항
     * type : "video" 인 항목들
     * GPString태그 VID7속성 찾는 xpath구문
     * CRCUnitArr태그 Name속성 찾는 구문으로 변환
     * > 멀티라인 텍스트 유사도 판별하기 어려움
     */
    if (search !== undefined) {
      let result = findSimilarString(gmepXmlDoc, search, 0.8);
      if (result !== null) {
        result = result.replace(/"/g, "'");
        search = result;
        ele = ele?.replace(/{search}/g, search);
      }
      else {
        ele = ele?.includes('{search}') ? null : ele;
      }
    }

    // xpath
    if (ele === 'none') {
      scoringResult[key] = "확인필요";
      continue;
    }

    if (type == "oneAnswer") {
      const result = xpath.select1(ele, gmepXmlDoc);

      // userAnswer = {};
      if ("speed" in rightAnswer) {
        userAnswer = {
          "speed": result ? result.value : null,
        };
      }
      else {
        userAnswer = result ? result.value : null;
      }

      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }

    // [3-1] 문항
    else if (type == "mediaOrder") {
      // 미디어 순서를 저장할 배열
      const mediaOrderList = [];

      // 미디어의 인덱스 순서
      const clipIndexOrder = xpath.select(ele, gmepXmlDoc);

      clipIndexOrder.forEach((clipIndex) => {
        CRClipIndex = parseInt(clipIndex.value, 10) + 1; // XPath는 1-based index를 사용

        // 인덱스 순서에 따른 CRClip 요소의 Path를 찾기
        const mediaPath = xpath.select1(`//CRClipArr/CRClip[${CRClipIndex}]/@Path`, gmepXmlDoc);

        // 만약 CRClip 요소가 motion clip인 경우 CRCUnitArr의 Path를 찾기
        if (mediaPath == null) {
          const motionClipPath = xpath.select1(`//CRClipArr/CRClip[${CRClipIndex}]/CRCUnitArr/@Path`, gmepXmlDoc);
          if (motionClipPath != null) {
            const fileName = path.basename(motionClipPath.value);
            mediaOrderList.push(fileName);
          }
        }
        else if (mediaPath != null) {
          const fileName = path.basename(mediaPath.value);
          mediaOrderList.push(fileName);
        }
      });
      const userAnswer = mediaOrderList;
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, {
        type: 'string[]'
      });
    }

    else if (type == "startEnd") {
      const crclipIndex = getCRClipIndex(media);
      // 해당 미디어가 없을경우 clipIndex값 -1
      if (crclipIndex == -1) {
        userAnswer = null;
      }
      else {
        //CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='동영상.mp4'] 요소를 찾음
        const xpathExpr = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const crTrackClip = xpath.select1(xpathExpr, gmepXmlDoc);
        if (!crTrackClip) {
          userAnswer = null;
        }
        else {
          // CRTrackClip 요소의 Pos(시작시간)과 Length(재생길이)를 구함
          const pos = xpath.select1('@Pos', crTrackClip);
          const length = xpath.select1('@Length', crTrackClip);

          userAnswer = {
            start: pos.value,
            end: length.value
          }
        }
      }
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }

    // 동영상 클립 이펙트 [2-4]
    // 이미지 클립 오버레이 [2-14, 17, 20]
    else if (type == "effect" || type === "imageOverlay") {
      const crclipIndex = getCRClipIndex(media);
      // 해당 미디어가 없을경우 clipIndex값 -1
      if (crclipIndex == -1) {
        userAnswer = null;
      }
      else {
        //CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='{CRClipIndex}']//CRFilter
        const xpathExpr = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const crFilter = xpath.select1(xpathExpr, gmepXmlDoc);
        if (!crFilter) {
          userAnswer = null;
          // totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
        }
        else {
          userAnswer = {}
          const attributes = crFilter.attributes;

          // rightAnswer의 key값을 순회하면서 
          // CRFilterNode의 속성명과 일치하는 값을 userAnswer에 저장
          // for (const [keyName, expectedValue] of Object.keys(rightAnswer)) {
          for (const keyName of Object.keys(rightAnswer)) {
            const attr = attributes.getNamedItem(keyName);
            userAnswer[keyName] = attr ? attr.value : null;
          }
          // totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
        }
      }
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }

    // 동영상 클립 트랜지션 [2-15, 18, 21]
    else if (type === 'clipTransition') {
      const crclipIndex = getCRClipIndex(media);
      let crtrackClipIndex = getCRTrackClipIndex(crclipIndex, "비디오1");
      let isMatched = false;

      if (crtrackClipIndex == -1) {
        userAnswer = null;
      }
      else {
        // 트랜지션 위치
        //  - 앞으로 이동 : Type = '1'
        //  - 뒤로 이동 : Type = '2'
        //  - 오버랩 : Type = '16'
        // 트랜지션 위치가 오버랩 일 경우 ( Type 속성이 16인 경우 )
        // 적용된 이미지(ClipIndex속성값은) 끝나는 지점의 이미지로 적용된다
        if (rightAnswer['Type'] === '16') {
          crtrackClipIndex += 1;
        }
        const xpathExpr = ele?.replace(/{CRTrackClipIndex}/g, crtrackClipIndex);
        const crTransFilter = xpath.select(xpathExpr, gmepXmlDoc);
        if (!crTransFilter) {
          userAnswer = null;
        }
        else {
          userAnswer = {};

          for (let i = 0; i < crTransFilter.length; i++) {
            const crTransFilterNode = crTransFilter[i];
            const attributes = crTransFilterNode.attributes;

            const idAttr = attributes.getNamedItem('ID');
            const typeAttr = attributes.getNamedItem('Type');
            const rangeAttr = attributes.getNamedItem('Range');

            const userId = idAttr ? idAttr.value : null;
            const userType = typeAttr ? typeAttr.value : null;
            const userRange = rangeAttr ? rangeAttr.value : null;

            userAnswer = {
              ID: userId,
              Range: userRange,
              Type: userType,
            };

            // ID와 Type은 같고, Range(재생시간 = 종료시간 - 시작시간)도 일치하는지 확인
            if (
              userId === rightAnswer.ID &&
              userType === rightAnswer.Type &&
              userRange &&
              rightAnswer.Range
            ) {
              const [start1, end1] = userRange.split(':').map(Number);
              const [start2, end2] = rightAnswer.Range.split(':').map(Number);

              const userPlayTime = end1 - start1;
              const rightPlayTime = end2 - start2;

              if (userPlayTime === rightPlayTime) {
                isMatched = true;
                break;
              }
            }
          }
        }
      }
      // 일치하지 않으면 null 처리
      if (!isMatched) {
        // userAnswer = null;
        totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
      }
      else {
        totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, {
          type: 'force-correct'
        });
      }
    }

    else if (type == "Mute") {
      const crclipIndex = getCRClipIndex(media);
      if (crclipIndex == -1) {
        userAnswer = null;
      }
      else {
        const xpathExpr = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const muteStatus = xpath.select1(xpathExpr, gmepXmlDoc);
        userAnswer = muteStatus?.value;
      }
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }
    // 이미지 클립 길이 [2-13, 16, 19]
    else if (type === 'imageLength') {
      const crclipIndex = getCRClipIndex(media);
      if (crclipIndex == -1) {
        userAnswer = null;
      }
      else {
        const xpathExpr = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const imageLength = xpath.select1(xpathExpr, gmepXmlDoc);
        userAnswer = parseInt(imageLength?.value ?? '0', 10);
      }
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }

    else if (type.includes('audio')) {
      const crclipIndex = getCRClipIndex(media);
      const crtrackClipIndex = getCRTrackClipIndex(crclipIndex, "오디오1")
      const startTimeList = getStartTimeList('오디오1');

      if (type.includes('StartTime')) {
        const startTime = startTimeList[crtrackClipIndex];
        userAnswer = startTime;
      }
      else if (type.includes('EndTime')) {
        const xpathQuery = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const crTrackClip = xpath.select1(xpathQuery, gmepXmlDoc);
        if (!crTrackClip) {
          userAnswer = null;
        } else {
          const length = crTrackClip.getAttribute('Length');
          userAnswer = parseInt(length ?? '0', 10);
        }
      }
      else if (type.includes('Effect')) {
        const xpathQuery = ele?.replace(/{CRClipIndex}/g, crclipIndex);
        const fadeoutEffect = xpath.select1(xpathQuery, gmepXmlDoc);
        if (!fadeoutEffect) {
          userAnswer = null;
        }
        else {
          const attributes = fadeoutEffect.attributes;
          const id = attributes.getNamedItem('ID').value;
          const duration = attributes.getNamedItem('VID8')?.value;

          userAnswer = {
            "ID": id,
            "Duration": duration,
          };
        }
      }
      totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
    }

    else if (type.includes('opening') || type.includes('video')) {
      // 자막의 정보를 이용해 CROwneUnit의 인덱스를 구함
      // 1. 텍스트 
      // 2. 순서
      // 3. 시작시간
      const indexByText = getClipIndexByText(search);
      const indexByOrder = getClipIndexByOrder(subtitleOrder);

      if (type.includes('opening')) time = openingStartTime;
      else if (type.includes('video')) time = videoStartTime;
      else time = null;
      const indexByStartTime = getCilpIndexByStartTime(time);

      // 1, 2, 3순으로 자막을 찾음


      // 권수혁 김재은 27, 28, 29 문제 오답 0점 채점 안되는 부분 처리
      // 06.18(목) 시작


      const index = indexByText ?? indexByOrder ?? indexByStartTime;
      if (index != null) {
        // 자막 시작시간 [2-10] [2-28]
        // <CRTrackClip> 요소들의 시작시간을 갖고 있는 startTimeList를 구하고
        // 해당 인덱스의 자막 시작시간을 구함
        if (type.includes('StartTime')) {
          const crtrackClipIndex = getCRTrackClipIndex(index, "텍스트")
          const startTimeList = getStartTimeList('텍스트');
          const startTime = startTimeList[crtrackClipIndex];

          userAnswer = startTime;
          totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
        }
        // 자막 길이 [2-11] [2-29]
        // <CRTrackClip> 요소의 Length 속성을 구함
        else if (type.includes('Length')) {
          const crtrackClipIndex = getCRTrackClipIndex(index, "텍스트") + 1 // XML 1-based index
          const clipLength = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[${crtrackClipIndex}]/@Length`, gmepXmlDoc);

          userAnswer = parseInt(clipLength?.value ?? '0', 10);
          totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
        }
        // 자막 텍스트(글자, 폰트, 크기, 색상) [2-5, 6, 7, 8] [2-22, 23, 24, 25]
        else if (type.includes('Text')) {
          const xmlIndex = index + 1 // XML 1-based index
          const subtitleXpath = ele?.replace(/{index}/g, xmlIndex);
          const subtitleResult = xpath.select1(subtitleXpath, gmepXmlDoc);

          if (subtitleResult) {
            if (type.includes('Color')) {
              const hex = convertColorToHex(subtitleResult.value);
              userAnswer = hex;
            }
            else if (type.includes('Outline')) {
              const attributes = subtitleResult.attributes;
              const width = Math.round(parseFloat(attributes.getNamedItem('VID100').value * 100)).toString();
              const color = convertColorToHex(attributes.getNamedItem('VID101').value);

              userAnswer = {
                "width": width,
                "color": color,
              };
            }
            else if (type.includes('FadeInEffect')) {
              const attributes = subtitleResult.attributes;
              const VID505 = attributes.getNamedItem('VID505').value;
              const VID507 = attributes.getNamedItem('VID507').value;

              userAnswer = {
                "VID505": VID505,
                "VID507": VID507,
              };
            }
            else {
              userAnswer = subtitleResult.value;
            }
          }
          else {
            userAnswer = null;
          }

          totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
        }

        // 자막 위치 [2-9] (화면 정가운데 아래)
        // 정답 좌표를 기준으로 
        else if (type.includes('Location')) {
          const xmlIndex = index + 1 // XML 1-based index
          const subtitleXpath = ele?.replace(/{index}/g, xmlIndex);
          const subtitleResult = xpath.select(subtitleXpath, gmepXmlDoc);

          userAnswer = subtitleResult.map(r => r.value);
          const errorRange = 0.1;

          // userAnswer가 정수형 배열인 경우 type을 'number[]'로 설정
          totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, {
            tolerance: errorRange,
            type: 'number[]',
          });
        }
      }
      else {
        userAnswer = null;
        totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
      }
    }


  }
  scoringResult['총점'] = totalScore;
  return scoringResult;
}

// psdData를 scoring.json 파일 내에 있는 ele 요소의 jsonpath로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산
// 학생 별로 psdData는 2개씩 있으므로 PSD 파일 1번과 2번에 대해서 채점
// scoring.json 파일에 있는 항목 수만큼 점수를 계산
// scoring.json 파일의 1번째 배열은 PSD 파일 1번에 대한 점수 계산
// scoring.json 파일의 2번째 배열은 PSD 파일 2번에 대한 점수 계산
// 채점 결과를 scoringResultList 배열에 저장
function getScore(psdData, scoring, index) {
  const psdTree = psdData[index].tree().export();
  const jsonData = JSON.stringify(psdTree, null, 2);
  // console.log(jsonData);
  const scoringResult = {};

  const scoringData = scoring[index];
  let totalScore = 0;
  for (const key in scoringData) {
    const ele = scoringData[key].ele;
    const value = scoringData[key].value;
    const point = scoringData[key].point;
    const type = scoringData[key].type;

    if (ele === 'none') {
      scoringResult[key] = "확인필요";
      continue;
    }

    try {
      const result = jsonPath.query(psdTree, ele);
      console.log(`ele: ${ele}, value: ${value}   result: ${result}`);
      if (result.length == 0) {
        scoringResult[key] = 0;
        continue;
      }
      if (type == "size") {
        // console.log(`result ${result.length}`);
        if (result[0].height == value['height'] && result[0].width == value['width']) {
          scoringResult[key] = point;
          totalScore += point;
        }
        else {
          scoringResult[key] = 0;
        }
      }
      // value가 color code인 경우 R,G,B를 16진수로 변환하여 비교하고 같다면 점수 부여
      // value: "ffa200"
      // result: [255,162,0,255]
      // 255,162,0,255 -> ffa200
      else if (type == "color") {
        // console.log(`result ${result}`); // result 255,162,0,255
        const temp = result[0].slice(0, 3).join(','); // 255,162,0

        // RGB의 각 색상값이 한자리수 일 경우 0을 채워 두자리로 만듬
        color = temp.split(',').map(v => parseInt(v).toString(16).padStart(2, '0')).join(''); // ffa200
        //  ffa20 -> ffa200
        // if (color.length == 5) {
        //   color = color + '0';
        // }

        // console.log(`color: ${color}`);
        if (color === value) {
          scoringResult[key] = point;
          totalScore += point;
        }
        else {
          scoringResult[key] = 0;
        }
      }
      // type이 font인 경우 font의 이름만 추출하여 비교
      // value: "Arial"
      // result: ["Arial-BoldItalicMT"]
      else if (type == "font") {
        const font = result[0].split('-')[0];
        // console.log(`result ${result}`);
        // console.log(`font: ${font}`);

        if (font === value) {
          scoringResult[key] = point;
          totalScore += point;
        }
        else {
          scoringResult[key] = 0;
        }
        // font가 여러개일 경우
        // scoringResult[key] = result.length > 0 && value === font ? point : 0;
      }
      else if (result[0] === value) {
        scoringResult[key] = point;
        totalScore += point;
      } else {
        scoringResult[key] = 0;
      }
    } catch (error) {
      console.error(`Error processing JSONPath query for ele: ${ele}`, error);
      scoringResult[key] = 0;
    }
  }

  scoringResult['총점'] = totalScore;
  return scoringResult;
}

/**
 * 자막 태그의 인덱스를 구할 때 사용
 * 1. CRTrackClip 요소의 순서에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함
 * 2. CRTrackClip 요소의 시작시간에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함  
 */
function getTrackClipNode(xmlDoc, type, videoStartTime, openingStartTime) {
  let trackClipNode = null;

  // 동영상 자막이면 2, 오프닝 자막이면 1, 그 외는 0
  const subtitleOrder = type === 'video' ? 2 : type === 'opening' ? 1 : null;
  const startTime = type === 'video' ? videoStartTime : openingStartTime;

  // xpath 구문을 통해 CRTrackClip 요소의 ClipIndex를 찾음
  const trackClipNode1 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[sum(preceding-sibling::CRTrackClip/@Length) = ${startTime}]`, xmlDoc);
  const trackClipNode2 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@ClipIndex='-1')][${subtitleOrder}]`, xmlDoc);

  return trackClipNode = trackClipNode1 ?? trackClipNode2;
}

/**
 * 자막텍스트를 이용해 자막 태그의 인덱스를 구할 때 사용
 * 1. 자막 텍스트의 유사도를 판별
 * 2. 자막텍스트와 일치하는 자막요소(CROWneUnit)의 순서를 구함
 */
function getTextClipIndex(xmlDoc, search) {
  // 1. search값이 일치하지 않는 경우 : count가 0이 되어 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [오류]
  // 2. search값이 일치하는 경우 
  //  1) search값이 CROwneUnit[1]이면 : preceding-sibling::CROwneUnit이 없어서 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [정상]
  //  2) search값이 CROwneUnit[2]이면 : preceding-sibling::CROwneUnit이 한개 있으므로 @ClipIndex = 1 / CROwneUnit[2]을 가리킴 [정상] ...
  if (!search) {
    return null;
  }
  const searchResult = search ? findSimilarString(xmlDoc, search, 0.8) : null;
  const cROwneUnitPreceding = searchResult ? xpath.select(`//CROwneUnit[CRCUnitArr[@Name='${searchResult}']]/preceding-sibling::CROwneUnit`, xmlDoc) : null;

  const clipIndex = cROwneUnitPreceding ? cROwneUnitPreceding.length : null;
  return clipIndex;
}

/**
 * scoringResultList 배열을 엑셀에 출력하기 위한 데이터 정리 함수
 * @param {Array} scoringResultList - 학생별 채점 결과 리스트
 * @returns {Array} - 엑셀에 출력할 데이터 배열
 */
function prepareExcelData(scoringResultList) {
  return scoringResultList.map(student => {
    // const flattened = { "학생": student["0"] }; // 학생 이름을 첫 번째 열로 설정
    const flattened = { "문항": student["0"] }; // 행열을 변환 할 경우 첫 행의 제목을 "문항"으로 설정

    // 제외할 키와 서브키 정의
    const exceptKeys = [
      "0", // 학생 이름 제외
      // "1", // psd1
      // "2", // psd2
    ];
    const exceptSubkeys = ["videoStartTime", "openingStartTime"]; // 제외할 서브키

    // 학생 데이터 순회
    Object.keys(student).forEach(key => {
      if (exceptKeys.includes(key)) {
        return; // 제외할 키는 건너뜀
      }

      // 서브키 순회
      if (typeof student[key] === "object") {
        Object.keys(student[key]).forEach(subKey => {
          if (exceptSubkeys.includes(subKey)) {
            return; // 제외할 서브키는 건너뜀
          }
          flattened[`${key}-${subKey}`] = student[key][subKey];
        });
      } else {
        // 서브키가 없는 경우
        flattened[key] = student[key];
      }
    });
    return flattened;
  });
}

// 데이터 행렬 변환
function transposeData(data) {
  // 데이터가 없으면 빈 배열 반환
  if (data.length === 0) return [];

  // 첫 번째 객체의 키(열 제목) 가져오기
  const keys = Object.keys(data[0]);

  // 행과 열을 변환
  const transposed = keys.map(key => {
    const row = { "항목": key }; // 각 열 제목을 "항목"으로 설정
    data.forEach((item, index) => {
      //console.log(data[index]['문항']);
      row[data[index]['문항']] = item[key]; // 각 학생의 데이터를 열로 추가
    });
    return row;
  });

  return transposed;
}

