
/**
 * 
 * @author
 * r4isen1920
 * https://mcpedl.com/user/r4isen1920
 * 
 * @license
 * MIT License
 * 
 */

import {
  LocationOutOfWorldBoundariesError,
  TicksPerSecond,
  system,
  world
} from '@minecraft/server';
import { toTitle, zFill, prettyCaps } from './utils';

import { blockIds } from './data/blockIds';
import { blockTools } from './data/blockTools';
import { entityPortraits } from './data/entityPortraits';
import { nameAliases } from './data/nameAliases';


// Call after 5 seconds of initialization -- ensure this script gives way for other scripts to load first
system.runTimeout(() => {

  // Run every second tick
  system.runInterval(iterAllPlayers, 2)

}, TicksPerSecond * 5)


// Re-trigger display when entity is hurt and health variable is updated
world.afterEvents.entityHitEntity.subscribe(
  event => {
    const { damagingEntity } = event;

    // Ignore if not player
    if (damagingEntity.typeId !== 'minecraft:player') return;

    // Add tag to player to reset UI
    damagingEntity.addTag('r4ui_reset');
  }
)


/**
 * 
 * Requests the process for each player
 * in the world
 */
function iterAllPlayers() {

  const MAX_DISTANCE = 8;

  world.getAllPlayers().forEach(
    player => {

      // Obtain entity or block the player is looking at
      let lookAtObject = fetchLookAt(player, MAX_DISTANCE);

      // Reset UI when needed
      if (player.hasTag('r4ui_reset')) { lookAtObject.hit = 'none'; player.removeTag('r4ui_reset'); }

      // Inform UI that there is nothing on the screen the player is looking at
      if (!lookAtObject) return;
      if (lookAtObject.hit === undefined) lookAtObject.hit = 'none';

      // Render the UI in the screen
      displayUI(player, lookAtObject);

    }
  )

}


/**
 * 
 * Parses the item AUX value of
 * the block or item
 * 
 * @param { import('@minecraft/server').Block | import('@minecraft/server').ItemStack } type
 * 
 * @returns { number | undefined }
 */
function getItemAux(type) {

  /* NOTICE: `Block.typeId` requires experimental -- but we will use it for now anyway since there's not much choice */
  let _a = blockIds.get(type?.typeId) * 65536;

  if (_a == undefined) return NaN;
  else return _a
}


/**
 * 
 * @param { import('@minecraft/server').Entity } entity 
 * @param { string } componentName 
 * 
 * @remarks
 * Returns the value of the
 * defined component from the entity
 * 
 * @returns { import('@minecraft/server').EntityComponent | '0' | undefined }
 */
function getComponentValue(entity, componentName) {
  const component = entity.getComponent(componentName);
  return component !== undefined ? component.value : '0';
}


/**
 * 
 * Parses the portrait of the entity
 * from variant and mark variant values
 * 
 * @param { import('@minecraft/server').Entity } entity 
 * 
 * @returns { string | undefined }
 */
function getEntityPortrait(entity) {

  if (!entity?.isValid()) return;

  const variant = getComponentValue(entity, 'variant');
  const markVariant = getComponentValue(entity, 'mark_variant');

  const baseKey = entity.typeId;
  let keys = [
    `${baseKey}:${variant}:${markVariant}`,
    `${baseKey}:${variant}:0`,
    `${baseKey}:0:${markVariant}`,
    `${baseKey}:0:0`
  ];

  // Checks if any of the keys format match from the dictionary
  for (let key of keys) {
    if (entityPortraits[key] !== undefined) {
      return entityPortraits[key];
    }
  }
  return '000';
}


/**
 * 
 * Parses the blockTools variable and returns
 * the type value if the input matches
 * or contains any value from the list.
 * 
 * @param { string } blockId 
 * 
 * @returns { string }
 */
function parseBlockTools(blockId) {
  for (const tools of blockTools) {
    if (
      tools.value.some(block => blockId.replace(blockId.replace(/(?<=:).+/g, ''), '').toString().includes(block)) ||
      tools.value.some(block => blockId.includes(block))
    ) return tools.type;
  }
  return 'unknown';
}


/**
 * 
 * Fetches what block or entity the
 * specified player is looking at
 * 
 * @param { import('@minecraft/server').Player } player 
 * The player to run the test against
 * @param { number } max_dist
 * Maximum range in blocks to check for
 * 
 * @returns { object }
 */
function fetchLookAt(player, max_dist) {

  let _a = {};

  try {

    // Fetch entity the player is looking at
    const entityLookAt = player.getEntitiesFromViewDirection({
      maxDistance: max_dist
    })


    if (entityLookAt.length > 0) {
      _a.type = 'entity'
      _a.rawHit = entityLookAt[0]?.entity
      _a.hit = _a.rawHit.typeId
    }

    if (_a.hit) return _a;


    // Fetch block the player is looking at
    const blockLookAt = player.getBlockFromViewDirection({
      includeLiquidBlocks: true,
      includePassableBlocks: true,
      maxDistance: max_dist
    })

    if (blockLookAt) {
      _a.type = 'tile'
      _a.rawHit = blockLookAt?.block
      _a.hit = _a.rawHit.typeId
    }

    return _a;

  } catch (e) {
    if (!e instanceof LocationOutOfWorldBoundariesError) throw e;
  }

}


/**
 * 
 * Draws the current health of
 * the entity in useable text
 * 
 * @param { number } currentHealth 
 * @param { number } maxHealth 
 * @returns { string }
 */
function healthRenderer(currentHealth, maxHealth) {

  /* The UI can render 40 HP at most -- adjust with caution */
  const MAX_LENGTH = 40;

  // Normalize value to the specified max length if it exceeds the value
  if (maxHealth > MAX_LENGTH) {
    currentHealth = Math.round((currentHealth / maxHealth) * MAX_LENGTH);
    maxHealth = MAX_LENGTH;
  }

  const healthIcons = {
    full: 'j',
    half: 'i',
    empty: 'h',
    padding: 'z'
  }

  // Count number of max, full, half-hearts and empty hearts beforehand
  const MAX_HEARTS_DISPLAY = Math.ceil(maxHealth / 2);
  let fullHearts = Math.floor(currentHealth / 2);
  let halfHearts = currentHealth % 2;
  let emptyHearts = MAX_HEARTS_DISPLAY - fullHearts - halfHearts;

  // Append them all together in one giant string
  let healthString = 
    healthIcons.full.repeat(fullHearts) + 
    healthIcons.half.repeat(halfHearts) + 
    healthIcons.empty.repeat(emptyHearts)

  healthString += healthIcons.padding.repeat(20 - MAX_HEARTS_DISPLAY);

  //* console.warn(`${currentHealth} / ${maxHealth}\n${healthString}`);

  if (healthString) return `:${healthString}`;
  else return `:zzzzzzzzzzzzzzzzzzzz`

}


/**
 * 
 * @param { object } lookAtObject 
 * @param { string } hitNamespace 
 * 
 * @returns { object }
 */
function fetchLookAtMetadata(lookAtObject, hitNamespace) {

  let _a = {};

  _a.type = lookAtObject.type;

  if (lookAtObject.type == 'entity') {

    // Set name depending on entity type that was hit
    switch (lookAtObject.hit) {
      case 'minecraft:player':
        _a.hit = lookAtObject.rawHit.name; break
      case 'minecraft:item': 
        const itemStackEntity = lookAtObject.rawHit.getComponent('item').itemStack;
        _a.hitItem = itemStackEntity.typeId;
        _a.itemAux = getItemAux(itemStackEntity)
      default:
        if (hitNamespace === 'minecraft:') _a.hit = `entity.${lookAtObject.hit}.name`;
        else _a.hit = lookAtObject.hit;
    }

    // Set entity family type
    _a.isInanimate = lookAtObject.rawHit.matches({ families: ['inanimate'] });

    // Set entity HP metadata
    const entityHp = lookAtObject.rawHit.getComponent('health')
    if (!_a.isInanimate) _a.healthRenderer = healthRenderer(Math.floor(entityHp?.currentValue), Math.floor(entityHp?.effectiveMax));
    else _a.healthRenderer = ':zzzzzzzzzzzzzzzzzzzz';

    // Set entity portrait metadata
    _a.entityPortrait = getEntityPortrait(lookAtObject.rawHit)

  } else if (lookAtObject.type == 'tile') {

    // Set default properties
    _a.hit = lookAtObject.hit;

    // Fetch block tool
    _a.tool = parseBlockTools(lookAtObject.hit)

    // Set block item AUX metadata
    _a.itemAux = getItemAux(lookAtObject.rawHit.getItemStack())

    // Set healthRenderer placeholder value
    _a.healthRenderer = ':zzzzzzzzzzzzzzzzzzzz';

  }

  return _a;

}


/**
 * 
 * Handles final string parse and
 * sends a request to the UI
 * 
 * @param { import('@minecraft/server').Player } player 
 * The player to render the UI onto
 * @param { object } lookAtObject 
 * Type of object to render
 */
function displayUI(player, lookAtObject) {

  // Fetch the namespace of the provided hit typeId
  const hitNamespace = lookAtObject.hit.replace(/(?<=:).+/g, '');

  // Only send a UI update if the value has changed
  const _L = lookAtObject.type === 'entity' ? JSON.stringify(lookAtObject) : lookAtObject.hit
  if (player.getDynamicProperty('r4:oldLog') !== _L) player.setDynamicProperty('r4:oldLog', _L); else return;

  // Remove information that was once displayed on screen
  if (lookAtObject.hit === 'none') {
    player.onScreenDisplay.setTitle(`_r4ui:hide`);
    return;
  }

  // Transform lookAtObject to a parsed object value with metadata included
  const object = fetchLookAtMetadata(lookAtObject, hitNamespace);

  // Parse text string information using template literals
  let parseStr = [];
  let parseStrSubtitle = [];
  const nameAlias = nameAliases.get(object.hit.replace(hitNamespace, ''));
  const iconTypes = {
    'sword': 'a',
    'axe': 'b',
    'pickaxe': 'c',
    'shovel': 'd',
    'hoe': 'e',
    'armor': 'f',
    'crops': 'g',
    'shears': 'k',
    'bucket': 'l',
    'brush': 'm',
    'unknown': 'z',
  }

  // Parse vanilla content
  if (hitNamespace === 'minecraft:') {

    parseStr = [
      { text: `_r4ui:${(object.type === 'tile' || object.hitItem) ? 'A' : 'B'}:` },
      { text: `${(object.type === 'tile' || object.hitItem) ? `${object.itemAux > 0 ? '' : '-'}${zFill(Math.abs(object.itemAux), object.itemAux > 0 ? 9 : 8)}` : object.entityPortrait}` },
      { text: `${object.type === 'tile' ? `:${iconTypes[object.tool]}:z:` : ':z:z:'}` },
      { translate: `${object.type === 'tile' ? `${nameAlias?.startsWith('item.') ? '' : 'tile.'}${!nameAlias ? object.hit.replace(hitNamespace, '') : nameAlias}.name` : object.hit.replace(hitNamespace, '')}` }
    ]
    parseStrSubtitle = [
      { text: `${object.healthRenderer}${(object.hitItem !== undefined ? `§7${object.hitItem}§r\n` : '')}` },
      { text: `${`§9§o${hitNamespace.length > 3 ? prettyCaps(toTitle(hitNamespace.replace(/_/, ' ').replace(':', ''))) : hitNamespace.replace(':', '').toUpperCase()}§r`}` }
    ]

  } else {

    parseStr = [

      // Parse non-vanilla content as such
      { text: `_r4ui:${object.type === 'tile' ? 'A' : 'B'}:` },
      { text: `${object.type === 'tile' ? '-00000NaN' : '000'}` },
      { text: `${object.type === 'tile' ? `:${iconTypes[object.tool]}:z:` : ':z:z:'}` },
      { translate: `${object.type}.${object.hit}.name` },
    ]
    parseStrSubtitle = [  
      { text: object.healthRenderer },
      { text: `${`§9§o${hitNamespace.length > 3 ? prettyCaps(toTitle(hitNamespace.replace(/_/, ' ').replace(':', ''))) : hitNamespace.replace(':', '').toUpperCase()}§r`}` }
    ]
  }

  //* console.warn(JSON.stringify(object))
  //* player.sendMessage(parseStr)
  //* player.sendMessage(parseStrSubtitle)

  // Pass the information on the JSON UI
  player.onScreenDisplay.setTitle(parseStr, {
    subtitle: parseStrSubtitle,

    fadeInDuration: 0,
    fadeOutDuration: 0,
    stayDuration: 0,
  });

}
