// @flow
/**
 * This is a declaration of an extension for GDevelop 5.
 *
 * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change
 * to this extension file or to any other *.js file that you reference inside.
 *
 * The file must be named "JsExtension.js", otherwise GDevelop won't load it.
 * ⚠️ If you make a change and the extension is not loaded, open the developer console
 * and search for any errors.
 *
 * More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
 */

/*::
// Import types to allow Flow to do static type checking on this file.
// Extensions declaration are typed using Flow (like the editor), but the files
// for the game engine are checked with TypeScript annotations.
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
*/

module.exports = {
  createExtension: function (
    _ /*: (string) => string */,
    gd /*: libGDevelop */
  ) {
    const extension = new gd.PlatformExtension();
    extension
      .setExtensionInformation(
        'TileMap',
        _('Tilemap'),
        "The Tilemap object can be used to display tile-based objects. It's a good way to create maps for RPG, strategy games or create objects by assembling tiles, useful for platformer, retro-looking games, etc...",
        'Todor Imreorov',
        'Open source (MIT License)'
      )
      .setExtensionHelpPath('/objects/tilemap');

    var objectTileMap = new gd.ObjectJsImplementation();
    // $FlowExpectedError - ignore Flow warning as we're creating an object
    objectTileMap.updateProperty = function (
      objectContent,
      propertyName,
      newValue
    ) {
      if (propertyName === 'tilemapJsonFile') {
        objectContent.tilemapJsonFile = newValue;
        return true;
      }
      if (propertyName === 'tilesetJsonFile') {
        objectContent.tilesetJsonFile = newValue;
        return true;
      }
      if (propertyName === 'tilemapAtlasImage') {
        objectContent.tilemapAtlasImage = newValue;
        return true;
      }
      if (propertyName === 'displayMode') {
        objectContent.displayMode = newValue;
        return true;
      }
      if (propertyName === 'layerIndex') {
        objectContent.layerIndex = parseFloat(newValue);
        return true;
      }
      if (propertyName === 'animationSpeedScale') {
        objectContent.animationSpeedScale = parseFloat(newValue);
        return true;
      }
      if (propertyName === 'animationFps') {
        objectContent.animationFps = parseFloat(newValue);
        return true;
      }

      return false;
    };
    // $FlowExpectedError - ignore Flow warning as we're creating an object
    objectTileMap.getProperties = function (objectContent) {
      var objectProperties = new gd.MapStringPropertyDescriptor();

      objectProperties.set(
        'tilemapJsonFile',
        new gd.PropertyDescriptor(objectContent.tilemapJsonFile)
          .setType('resource')
          .addExtraInfo('json')
          .setLabel(_('Tilemap JSON file'))
          .setDescription(
            _('This is the JSON file that was saved or exported from Tiled.')
          )
      );
      objectProperties.set(
        'tilesetJsonFile',
        new gd.PropertyDescriptor(objectContent.tilesetJsonFile || '')
          .setType('resource')
          .addExtraInfo('json')
          .setLabel(_('Tileset JSON file (optional)'))
          .setDescription(
            _(
              "Optional, don't specify it if you've not saved the tileset in a different file."
            )
          )
      );
      objectProperties.set(
        'tilemapAtlasImage',
        new gd.PropertyDescriptor(objectContent.tilemapAtlasImage)
          .setType('resource')
          .addExtraInfo('image')
          .setLabel(_('Atlas image'))
      );
      objectProperties.set(
        'displayMode',
        new gd.PropertyDescriptor(objectContent.displayMode)
          .setType('choice')
          .addExtraInfo('visible')
          .addExtraInfo('all')
          .addExtraInfo('index')
          .setLabel(_('Display mode'))
      );
      objectProperties.set(
        'layerIndex',
        new gd.PropertyDescriptor(objectContent.layerIndex.toString())
          .setType('number')
          .setLabel(_('Layer index to display'))
          .setDescription(
            _(
              'If "index" is selected as the display mode, this is the index of the layer to display.'
            )
          )
      );
      objectProperties.set(
        'animationSpeedScale',
        new gd.PropertyDescriptor(objectContent.animationSpeedScale.toString())
          .setType('number')
          .setLabel(_('Animation speed scale'))
      );
      objectProperties.set(
        'animationFps',
        new gd.PropertyDescriptor(objectContent.animationFps.toString())
          .setType('number')
          .setLabel(_('Animation FPS'))
      );

      return objectProperties;
    };
    objectTileMap.setRawJSONContent(
      JSON.stringify({
        tilemapJsonFile: '',
        tilesetJsonFile: '',
        tilemapAtlasImage: '',
        displayMode: 'visible',
        layerIndex: 0,
        animationSpeedScale: 1,
        animationFps: 4,
      })
    );

    // $FlowExpectedError - ignore Flow warning as we're creating an object
    objectTileMap.updateInitialInstanceProperty = function (
      objectContent,
      instance,
      propertyName,
      newValue,
      project,
      layout
    ) {
      return false;
    };
    // $FlowExpectedError - ignore Flow warning as we're creating an object
    objectTileMap.getInitialInstanceProperties = function (
      content,
      instance,
      project,
      layout
    ) {
      var instanceProperties = new gd.MapStringPropertyDescriptor();
      return instanceProperties;
    };

    const object = extension
      .addObject(
        'TileMap',
        _('Tilemap'),
        _(
          'Displays a tiled-based map, made with the Tiled editor (download it separately on https://www.mapeditor.org/).'
        ),
        'JsPlatform/Extensions/tile_map32.png',
        objectTileMap
      )
      .setIncludeFile('Extensions/TileMap/tilemapruntimeobject.js')
      .addIncludeFile(
        'Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js'
      )
      .addIncludeFile(
        'Extensions/TileMap/pixi-tilemap/dist/pixi-tilemap.umd.js'
      )
      .addIncludeFile('Extensions/TileMap/pako/dist/pako.min.js')
      .addIncludeFile('Extensions/TileMap/pixi-tilemap-helper.js');

    object
      .addCondition(
        'TilemapJsonFile',
        _('Tilemap JSON file'),
        _('Check the Tilemap JSON file being used.'),
        _('The Tilemap JSON file of _PARAM0_ is _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter('jsonResource', _('Tilemap JSON file'), '', false)
      .getCodeExtraInformation()
      .setFunctionName('isTilemapJsonFile');

    object
      .addAction(
        'SetTilemapJsonFile',
        _('Tilemap JSON file'),
        _(
          'Set the JSON file containing the Tilemap data to display. This is usually the JSON file exported from Tiled.'
        ),
        _('Set the Tilemap JSON file of _PARAM0_ to _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter('jsonResource', _('Tilemap JSON file'), '', false)
      .getCodeExtraInformation()
      .setFunctionName('setTilemapJsonFile');

    object
      .addCondition(
        'TilesetJsonFile',
        _('Tileset JSON file'),
        _('Check the tileset JSON file being used.'),
        _('The tileset JSON file of _PARAM0_ is _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter('jsonResource', _('Tileset JSON file'), '', false)
      .getCodeExtraInformation()
      .setFunctionName('isTilesetJsonFile');

    object
      .addAction(
        'SetTilesetJsonFile',
        _('Tileset JSON file'),
        _(
          'Set the JSON file with the tileset data (sometimes that is embeded in the Tilemap, so not needed)'
        ),
        _('Set the tileset JSON file of _PARAM0_ to _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter('jsonResource', _('Tileset JSON file'), '', false)
      .getCodeExtraInformation()
      .setFunctionName('setTilesetJsonFile');

    object
      .addCondition(
        'DisplayMode',
        _('Display mode'),
        _('Compare the value of the display mode.'),
        _('The display mode of _PARAM0_ is _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter(
        'stringWithSelector',
        _('Display mode'),
        '["visible", "all", "index"]',
        false
      )
      .getCodeExtraInformation()
      .setFunctionName('isDisplayMode');

    object
      .addAction(
        'SetDisplayMode',
        _('Display mode'),
        _('Set the display mode'),
        _('Set the display mode of _PARAM0_ to _PARAM1_'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .addParameter(
        'stringWithSelector',
        _('Display mode'),
        '["visible", "all", "index"]',
        false
      )
      .getCodeExtraInformation()
      .setFunctionName('setDisplayMode');

    object
      .addCondition(
        'LayerIndex',
        _('Layer index'),
        _('Compare the value of the layer index.'),
        _('the layer index'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardRelationalOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('getLayerIndex');

    object
      .addAction(
        'SetLayerIndex',
        _('Layer index'),
        _('Set the layer index of the Tilemap.'),
        _('the layer index'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('setLayerIndex')
      .setGetter('getLayerIndex');

    object
      .addExpression(
        'LayerIndex',
        _('Layer index'),
        _('Get the layer index being displayed'),
        '',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .getCodeExtraInformation()
      .setFunctionName('getLayerIndex');

    object
      .addCondition(
        'AnimationSpeedScale',
        _('Animation speed scale'),
        _('Compare the animation speed scale.'),
        _('the animation speed scale'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardRelationalOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('getAnimationSpeedScale');

    object
      .addAction(
        'SetAnimationSpeedScale',
        _('Animation speed scale'),
        _('Set the animation speed scale of the Tilemap (1 by default).'),
        _('the animation speed scale'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('setAnimationSpeedScale')
      .setGetter('getAnimationSpeedScale');

    object
      .addExpression(
        'AnimationSpeedScale',
        _('Animation speed scale'),
        _('Get the Animation speed scale'),
        '',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .getCodeExtraInformation()
      .setFunctionName('getAnimationSpeedScale');

    object
      .addCondition(
        'AnimationFps',
        _('Animation speed (FPS)'),
        _('Compare the animation speed (in frames per second).'),
        _('the animation speed (FPS)'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardRelationalOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('getAnimationFps');

    object
      .addAction(
        'SetAnimationFps',
        _('Animation speed (FPS)'),
        _('Set the animation speed (in frames per second) of the Tilemap.'),
        _('the animation speed (FPS)'),
        '',
        'JsPlatform/Extensions/tile_map24.png',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .useStandardOperatorParameters('number')
      .getCodeExtraInformation()
      .setFunctionName('setAnimationFps')
      .setGetter('getAnimationFps');

    object
      .addExpression(
        'AnimationFps',
        _('Animation speed (FPS)'),
        _('Get the animation speed (in frames per second)'),
        '',
        'JsPlatform/Extensions/tile_map32.png'
      )
      .addParameter('object', 'TileMap', 'TileMap', false)
      .getCodeExtraInformation()
      .setFunctionName('getAnimationFps');

    return extension;
  },

  /**
   * You can optionally add sanity tests that will check the basic working
   * of your extension behaviors/objects by instanciating behaviors/objects
   * and setting the property to a given value.
   *
   * If you don't have any tests, you can simply return an empty array like this:
   * `runExtensionSanityTests: function(gd, extension) { return []; }`
   *
   * But it is recommended to create tests for the behaviors/objects properties you created
   * to avoid mistakes.
   */
  runExtensionSanityTests: function (
    gd /*: libGDevelop */,
    extension /*: gdPlatformExtension*/
  ) {
    return [];
  },
  /**
   * Register editors for objects.
   *
   * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
   */
  registerEditorConfigurations: function (
    objectsEditorService /*: ObjectsEditorService */
  ) {
    objectsEditorService.registerEditorConfiguration(
      'TileMap::TileMap',
      objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
        helpPagePath: '/objects/tile_map_object',
      })
    );
  },
  /**
   * Register renderers for instance of objects on the scene editor.
   *
   * ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
   */
  registerInstanceRenderers: function (
    objectsRenderingService /*: ObjectsRenderingService */
  ) {
    const RenderedInstance = objectsRenderingService.RenderedInstance;
    const PIXI = objectsRenderingService.PIXI;

    const Tilemap = objectsRenderingService.requireModule(
      __dirname,
      'pixi-tilemap/dist/pixi-tilemap.umd'
    );
    const PixiTilemapHelper = objectsRenderingService.requireModule(
      __dirname,
      'pixi-tilemap-helper'
    );
    // required for decoding tiled zlib compressed layer data
    const pako = objectsRenderingService.requireModule(
      __dirname,
      'pako/dist/pako.min'
    );

    /**
     * Renderer for instances of TileMap inside the IDE.
     *
     * @extends RenderedTileMapInstance
     * @class RenderedTileMapInstance
     * @constructor
     */
    function RenderedTileMapInstance(
      project,
      layout,
      instance,
      associatedObject,
      pixiContainer,
      pixiResourcesLoader
    ) {
      RenderedInstance.call(
        this,
        project,
        layout,
        instance,
        associatedObject,
        pixiContainer,
        pixiResourcesLoader
      );

      this._pixiObject = new Tilemap.CompositeRectTileLayer(0);

      // Implement `containsPoint` so that we can set `interactive` to true and
      // the Tilemap will properly emit events when hovered/clicked.
      // By default, this is not implemented in pixi-tilemap.
      this._pixiObject.containsPoint = (position) => {
        // Turns the world position to the local object coordinates
        const localPosition = new PIXI.Point();
        this._pixiObject.worldTransform.applyInverse(position, localPosition);

        // Check if the point is inside the object bounds
        const originalWidth = this._pixiObject.width / this._pixiObject.scale.x;
        const originalHeight =
          this._pixiObject.height / this._pixiObject.scale.y;

        return (
          localPosition.x >= 0 &&
          localPosition.x < originalWidth &&
          localPosition.y >= 0 &&
          localPosition.y < originalHeight
        );
      };
      this._pixiContainer.addChild(this._pixiObject);
      this.update();
      this.updateTileMap();
    }
    RenderedTileMapInstance.prototype = Object.create(
      RenderedInstance.prototype
    );

    /**
     * Return the path to the thumbnail of the specified object.
     */
    RenderedTileMapInstance.getThumbnail = function (
      project,
      resourcesLoader,
      object
    ) {
      return 'JsPlatform/Extensions/tile_map32.png';
    };

    /**
     * This is used to reload the Tilemap
     */
    RenderedTileMapInstance.prototype._loadTileMapWithTileset = function (
      tileMapJsonData,
      tilesetJsonData
    ) {
      // Get the tileset resource to use
      const tilemapAtlasImage = this._associatedObject
        .getProperties(this.project)
        .get('tilemapAtlasImage')
        .getValue();
      const tilemapJsonFile = this._associatedObject
        .getProperties(this.project)
        .get('tilemapJsonFile')
        .getValue();
      const layerIndex = parseInt(
        this._associatedObject
          .getProperties(this.project)
          .get('layerIndex')
          .getValue(),
        10
      );
      const displayMode = this._associatedObject
        .getProperties(this.project)
        .get('displayMode')
        .getValue();
      const tilesetJsonFile = this._associatedObject
        .getProperties(this.project)
        .get('tilesetJsonFile')
        .getValue();

      const pixiTileMapData = PixiTilemapHelper.loadPixiTileMapData(
        (textureName) =>
          this._pixiResourcesLoader.getPIXITexture(this._project, textureName),
        tilesetJsonData
          ? { ...tileMapJsonData, tilesets: [tilesetJsonData] }
          : tileMapJsonData,
        tilemapAtlasImage,
        tilemapJsonFile,
        tilesetJsonFile
      );

      if (pixiTileMapData) {
        PixiTilemapHelper.updatePixiTileMap(
          this._pixiObject,
          pixiTileMapData,
          displayMode,
          layerIndex,
          pako
        );
      }
    };

    RenderedTileMapInstance.prototype.updateTileMap = async function () {
      // Get the tileset resource to use
      const tilemapJsonFile = this._associatedObject
        .getProperties(this.project)
        .get('tilemapJsonFile')
        .getValue();
      const tilesetJsonFile = this._associatedObject
        .getProperties(this.project)
        .get('tilesetJsonFile')
        .getValue();

      try {
        const tileMapJsonData = await this._pixiResourcesLoader.getResourceJsonData(
          this._project,
          tilemapJsonFile
        );

        const tilesetJsonData = tilesetJsonFile
          ? await this._pixiResourcesLoader.getResourceJsonData(
              this._project,
              tilesetJsonFile
            )
          : null;

        this._loadTileMapWithTileset(tileMapJsonData, tilesetJsonData);
      } catch (err) {
        console.error('Unable to load a Tilemap JSON data: ', err);
      }
    };
    /**
     * This is called to update the PIXI object on the scene editor
     */
    RenderedTileMapInstance.prototype.update = function () {
      if (this._instance.hasCustomSize()) {
        this._pixiObject.width = this._instance.getCustomWidth();
        this._pixiObject.height = this._instance.getCustomHeight();
      } else {
        this._pixiObject.scale.x = 1;
        this._pixiObject.scale.y = 1;
      }

      // Place the center of rotation in the center of the object. Because pivot position in Pixi
      // is in the **local coordinates of the object**, we need to find back the original width
      // and height of the object before scaling (then divide by 2 to find the center)
      const originalWidth = this._pixiObject.width / this._pixiObject.scale.x;
      const originalHeight = this._pixiObject.height / this._pixiObject.scale.y;
      this._pixiObject.pivot.x = originalWidth / 2;
      this._pixiObject.pivot.y = originalHeight / 2;

      // Modifying the pivot position also has an impact on the transform. The instance (X,Y) position
      // of this object refers to the top-left point, but now in Pixi, as we changed the pivot, the Pixi
      // object (X,Y) position refers to the center. So we add an offset to convert from top-left to center.
      this._pixiObject.x = this._instance.getX() + this._pixiObject.width / 2;
      this._pixiObject.y = this._instance.getY() + this._pixiObject.height / 2;

      // Rotation works as intended because we put the pivot in the center
      this._pixiObject.rotation = RenderedInstance.toRad(
        this._instance.getAngle()
      );
    };

    /**
     * Return the width of the instance, when it's not resized.
     */
    RenderedTileMapInstance.prototype.getDefaultWidth = function () {
      return this._pixiObject.width / this._pixiObject.scale.x;
    };

    /**
     * Return the height of the instance, when it's not resized.
     */
    RenderedTileMapInstance.prototype.getDefaultHeight = function () {
      return this._pixiObject.height / this._pixiObject.scale.y;
    };

    objectsRenderingService.registerInstanceRenderer(
      'TileMap::TileMap',
      RenderedTileMapInstance
    );
  },
};
