"""
@package psmap.instructions

@brief Map feature objects

Classes:
 - dialogs::Instruction
 - dialogs::InstructionObject
 - dialogs::InitMap
 - dialogs::MapFrame
 - dialogs::PageSetup
 - dialogs::Mapinfo
 - dialogs::Text
 - dialogs::Image
 - dialogs::NorthArrow
 - dialogs::Point
 - dialogs::Line
 - dialogs::Rectangle
 - dialogs::Scalebar
 - dialogs::RasterLegend
 - dialogs::VectorLegend
 - dialogs::Raster
 - dialogs::Vector
 - dialogs::VProperties

(C) 2011-2012 by Anna Kratochvilova, and the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Anna Kratochvilova <kratochanna gmail.com> (bachelor's project)
@author Martin Landa <landa.martin gmail.com> (mentor)
"""

import os
import string
from math import ceil
from pathlib import Path
from time import localtime, strftime

import grass.script as gs
import wx
from core.gcmd import GError, GMessage, GWarning
from core.utils import GetCmdString
from dbmgr.vinfo import VectorDBInfo
from grass.exceptions import ScriptError
from grass.script.task import cmdlist_to_tuple
from gui_core.wrap import NewId as wxNewId
from psmap.utils import (  # Add any additional required names from psmap.utils here
    BBoxAfterRotation,
    GetMapBounds,
    PaperMapCoordinates,
    Rect2D,
    Rect2DPP,
    SetResolution,
    UnitConversion,
    getRasterType,
    projInfo,
)


def NewId():
    return int(wxNewId())


class Instruction:
    """Class which represents instruction file"""

    def __init__(self, parent, objectsToDraw, env):
        self.parent = parent
        self.objectsToDraw = objectsToDraw
        # here are kept objects like mapinfo, rasterlegend, etc.
        self.instruction = []
        self.env = env

    def __str__(self):
        """Returns text for instruction file"""
        comment = "# timestamp: " + strftime("%Y-%m-%d %H:%M", localtime()) + "\n"
        env = gs.gisenv()
        comment += "# location: %s\n" % env["LOCATION_NAME"]
        comment += "# mapset: %s\n" % env["MAPSET"]
        comment += (
            "# page orientation: %s\n"
            % self.FindInstructionByType("page")["Orientation"]
        )
        border = ""
        if not self.FindInstructionByType("map"):
            border = "border n\n"
        text = [str(each) for each in self.instruction]
        return comment + border + "\n".join(text) + "\nend"

    def __getitem__(self, id):
        for each in self.instruction:
            if each.id == id:
                return each
        return None

    def __contains__(self, id):
        """Test if instruction is included"""
        return any(each.id == id for each in self.instruction)

    def __delitem__(self, id):
        """Delete instruction"""
        for each in self.instruction:
            if each.id != id:
                continue
            if each.type == "map":
                # must remove raster, vector layers, labels too
                vektor = self.FindInstructionByType("vector", list=True)
                vProperties = self.FindInstructionByType("vProperties", list=True)
                raster = self.FindInstructionByType("raster", list=True)
                labels = self.FindInstructionByType("labels", list=True)
                for item in vektor + vProperties + raster + labels:
                    if item in self.instruction:
                        self.instruction.remove(item)

            self.instruction.remove(each)
            if id in self.objectsToDraw:
                self.objectsToDraw.remove(id)
            return

    def AddInstruction(self, instruction):
        """Add instruction"""
        # add to instructions
        if instruction.type == "map":
            self.instruction.insert(0, instruction)
        else:
            self.instruction.append(instruction)
        # add to drawable objects
        if instruction.type not in {
            "page",
            "raster",
            "vector",
            "vProperties",
            "initMap",
            "labels",
        }:
            if instruction.type == "map":
                self.objectsToDraw.insert(0, instruction.id)
            else:
                self.objectsToDraw.append(instruction.id)

    def FindInstructionByType(self, type, list=False):
        """Find instruction(s) with the given type"""
        inst = [each for each in self.instruction if each.type == type]
        if len(inst) == 1 and not list:
            return inst[0]
        return inst

    def Read(self, filename):
        """Reads instruction file and creates instruction objects"""
        self.filename = filename
        # open file
        try:
            content = Path(filename).read_text(encoding="Latin_1", errors="ignore")
        except OSError:
            GError(message=_("Unable to open file\n%s") % filename)
            return None
        # first read file to get information about region and scaletype
        isRegionComment = False
        orientation = "Portrait"
        for line in content.splitlines():
            if "# g.region" in line:
                self.SetRegion(regionInstruction=line)
                isRegionComment = True
                break
            if "# page orientation" in line:
                orientation = line.split(":")[-1].strip()

        if not isRegionComment:
            self.SetRegion(regionInstruction=None)
        # then run ps.map -b to get information for maploc
        # compute scale and center
        map = self.FindInstructionByType("map")
        region = gs.region(env=self.env)
        map["center"] = (region["n"] + region["s"]) / 2, (region["w"] + region["e"]) / 2
        mapRect = GetMapBounds(
            self.filename, portrait=(orientation == "Portrait"), env=self.env
        )
        map["rect"] = mapRect
        proj = projInfo()
        toM = 1.0
        if proj["units"]:
            toM = float(proj["meters"])
        units = UnitConversion(self.parent)
        w = units.convert(value=mapRect.Get()[2], fromUnit="inch", toUnit="meter") / toM
        map["scale"] = w / abs(region["w"] - region["e"])

        SetResolution(
            dpi=300, width=map["rect"].width, height=map["rect"].height, env=self.env
        )

        # read file again, now with information about map bounds
        isBuffer = False
        buffer = []
        instruction = None
        vectorMapNumber = 1
        for line in content.splitlines():
            if not line.strip():
                continue
            line = line.strip()
            if isBuffer:
                buffer.append(line)
                if "end" in line:
                    isBuffer = False
                    kwargs = {}
                    if instruction == "scalebar":
                        kwargs["scale"] = map["scale"]
                    elif instruction in {"text", "eps", "point", "line", "rectangle"}:
                        kwargs["mapInstruction"] = map
                    elif instruction in {"vpoints", "vlines", "vareas"}:
                        kwargs["id"] = NewId()
                        kwargs["vectorMapNumber"] = vectorMapNumber
                        vectorMapNumber += 1
                    elif instruction == "paper":
                        kwargs["Orientation"] = orientation

                    ok = self.SendToRead(instruction, buffer, **kwargs)
                    if not ok:
                        return False
                    buffer = []
                continue

            if line.startswith("paper"):
                instruction = "paper"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("border"):
                if line.split()[1].lower() in {"n", "no", "none"}:
                    ok = self.SendToRead("border", [line])
                    if not ok:
                        return False
                elif line.split()[1].lower() in {"y", "yes"}:
                    instruction = "border"
                    isBuffer = True
                    buffer.append(line)

            elif line.startswith("scale "):
                if isBuffer:
                    continue
                ok = self.SendToRead("scale", line, isRegionComment=isRegionComment)
                if not ok:
                    return False

            elif line.startswith("maploc"):
                ok = self.SendToRead(instruction="maploc", text=line)
                if not ok:
                    return False

            elif line.startswith("raster"):
                ok = self.SendToRead(instruction="raster", text=line)
                if not ok:
                    return False

            elif line.startswith("mapinfo"):
                instruction = "mapinfo"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("scalebar"):
                instruction = "scalebar"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("text"):
                instruction = "text"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("eps"):
                instruction = "eps"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("point"):
                instruction = "point"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("line"):
                instruction = "line"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("rectangle"):
                instruction = "rectangle"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("colortable"):
                if len(line.split()) == 2 and line.split()[1].lower() in {
                    "n",
                    "no",
                    "none",
                }:
                    break
                instruction = "colortable"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("vlegend"):
                instruction = "vlegend"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("vpoints"):
                instruction = "vpoints"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("vlines"):
                instruction = "vlines"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("vareas"):
                instruction = "vareas"
                isBuffer = True
                buffer.append(line)

            elif line.startswith("labels"):
                instruction = "labels"
                isBuffer = True
                buffer.append(line)

        rasterLegend = self.FindInstructionByType("rasterLegend")
        raster = self.FindInstructionByType("raster")
        page = self.FindInstructionByType("page")
        vector = self.FindInstructionByType("vector")
        vectorLegend = self.FindInstructionByType("vectorLegend")
        vectorMaps = self.FindInstructionByType("vProperties", list=True)

        # check (in case of scaletype 0) if map is drawn also
        map["drawMap"] = False
        if map["scaleType"] == 0:
            mapForRegion = map["map"]
            if map["mapType"] == "raster" and raster:
                if mapForRegion == raster["raster"]:
                    map["drawMap"] = True
            elif map["mapType"] == "vector" and vector:
                for vmap in vector["list"]:
                    if mapForRegion == vmap[0]:
                        map["drawMap"] = True

        # rasterLegend
        if rasterLegend:
            if rasterLegend["rasterDefault"] and raster:
                rasterLegend["raster"] = raster["raster"]
                if not rasterLegend["discrete"]:
                    rasterType = getRasterType(map=rasterLegend["raster"])
                    if rasterType == "CELL":
                        rasterLegend["discrete"] = "y"
                    else:
                        rasterLegend["discrete"] = "n"

            # estimate size
            height = rasterLegend.EstimateHeight(
                raster=rasterLegend["raster"],
                discrete=rasterLegend["discrete"],
                fontsize=rasterLegend["fontsize"],
                cols=rasterLegend["cols"],
                height=rasterLegend["height"],
            )
            width = rasterLegend.EstimateWidth(
                raster=rasterLegend["raster"],
                discrete=rasterLegend["discrete"],
                fontsize=rasterLegend["fontsize"],
                cols=rasterLegend["cols"],
                width=rasterLegend["width"],
                paperInstr=page,
            )
            rasterLegend["rect"] = Rect2D(
                x=float(rasterLegend["where"][0]),
                y=float(rasterLegend["where"][1]),
                width=width,
                height=height,
            )

        # vectors, vlegend

        if vector:
            for vmap in vectorMaps:
                for i, each in enumerate(vector["list"]):
                    if each[2] == vmap.id:
                        vector["list"][i][4] = vmap["label"]
                        vector["list"][i][3] = vmap["lpos"]
            if vectorLegend:
                size = vectorLegend.EstimateSize(
                    vectorInstr=vector,
                    fontsize=vectorLegend["fontsize"],
                    width=vectorLegend["width"],
                    cols=vectorLegend["cols"],
                )
                vectorLegend["rect"] = Rect2D(
                    x=float(vectorLegend["where"][0]),
                    y=float(vectorLegend["where"][1]),
                    width=size[0],
                    height=size[1],
                )

        page = self.FindInstructionByType("page")
        if not page:
            page = PageSetup(NewId(), env=self.env)
            self.AddInstruction(page)
        else:
            page["Orientation"] = orientation

        return True

    def SendToRead(self, instruction, text, **kwargs):
        psmapInstrDict = {
            "paper": ["page"],
            "maploc": ["map"],
            "scale": ["map"],
            "border": ["map"],
            "raster": ["raster"],
            "mapinfo": ["mapinfo"],
            "scalebar": ["scalebar"],
            "text": ["text"],
            "eps": ["image", "northArrow"],
            "point": ["point"],
            "line": ["line"],
            "rectangle": ["rectangle"],
            "vpoints": ["vector", "vProperties"],
            "vlines": ["vector", "vProperties"],
            "vareas": ["vector", "vProperties"],
            "colortable": ["rasterLegend"],
            "vlegend": ["vectorLegend"],
            "labels": ["labels"],
        }

        myInstrDict = {
            "page": PageSetup,
            "map": MapFrame,
            "raster": Raster,
            "mapinfo": Mapinfo,
            "scalebar": Scalebar,
            "text": Text,
            "image": Image,
            "northArrow": NorthArrow,
            "point": Point,
            "line": Line,
            "rectangle": Rectangle,
            "rasterLegend": RasterLegend,
            "vectorLegend": VectorLegend,
            "vector": Vector,
            "vProperties": VProperties,
            "labels": Labels,
        }

        myInstruction = psmapInstrDict[instruction]

        for i in myInstruction:
            instr = self.FindInstructionByType(i)
            if (
                i
                in {
                    "text",
                    "vProperties",
                    "image",
                    "northArrow",
                    "point",
                    "line",
                    "rectangle",
                }
                or not instr
            ):
                id = NewId()  # !vProperties expect subtype
                if i == "vProperties":
                    id = kwargs["id"]
                    newInstr = myInstrDict[i](
                        id,
                        subType=instruction[1:],
                        env=self.env,
                    )
                elif i in {"image", "northArrow"}:
                    commentFound = False
                    for line in text:
                        if line.find("# north arrow") >= 0:
                            commentFound = True
                    if (i == "image" and commentFound) or (
                        i == "northArrow" and not commentFound
                    ):
                        continue
                    newInstr = myInstrDict[i](id, settings=self, env=self.env)
                else:
                    newInstr = myInstrDict[i](id, env=self.env)
                ok = newInstr.Read(instruction, text, **kwargs)
                if ok:
                    self.AddInstruction(newInstr)
                else:
                    return False

            else:
                ok = instr.Read(instruction, text, **kwargs)

                if not ok:
                    return False
        return True

    def SetRegion(self, regionInstruction):
        """Sets region from file comment or sets current region in case of no comment"""
        map = MapFrame(NewId(), env=self.env)
        self.AddInstruction(map)
        if regionInstruction:
            cmd = cmdlist_to_tuple(regionInstruction.strip("# ").split())

            # define scaleType
            if len(cmd[1]) <= 3:
                if "raster" in cmd[1]:
                    map["scaleType"] = 0
                    map["mapType"] = "raster"
                    map["map"] = cmd[1]["raster"]
                elif "vector" in cmd[1]:
                    map["scaleType"] = 0
                    map["mapType"] = "vector"
                    map["map"] = cmd[1]["vector"]
                elif "region" in cmd[1]:
                    map["scaleType"] = 1
                    map["region"] = cmd[1]["region"]

            else:
                map["scaleType"] = 2
        else:
            map["scaleType"] = 2
            region = gs.region(env=None)
            cmd = ["g.region", region]
        cmdString = GetCmdString(cmd).replace("g.region", "")
        GMessage(
            _("Instruction file will be loaded with following region: %s\n") % cmdString
        )
        try:
            self.env["GRASS_REGION"] = gs.region_env(env=self.env, **cmd[1])

        except ScriptError as e:
            GError(_("Region cannot be set\n%s") % e)
            return False


class InstructionObject:
    """Abstract class representing single instruction"""

    def __init__(self, id, env):
        self.id = id
        self.env = env

        # default values
        self.defaultInstruction = {}
        # current values
        self.instruction = self.defaultInstruction
        # converting units
        self.unitConv = UnitConversion()

    def __str__(self):
        """Returns particular part of text instruction"""
        return ""

    def __getitem__(self, key):
        for each in self.instruction.keys():
            if each == key:
                return self.instruction[key]
        return None

    def __setitem__(self, key, value):
        self.instruction[key] = value

    def GetInstruction(self):
        """Get current values"""
        return self.instruction

    def SetInstruction(self, instruction):
        """Set default values"""
        self.instruction = instruction

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save them"""

    def PercentToReal(self, e, n):
        """Converts text coordinates from percent of region to map coordinates"""
        e, n = float(e.strip("%")), float(n.strip("%"))
        region = gs.region(env=self.env)
        N = region["s"] + (region["n"] - region["s"]) / 100 * n
        E = region["w"] + (region["e"] - region["w"]) / 100 * e
        return E, N


class InitMap(InstructionObject):
    """Class representing virtual map"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "initMap"

        # default values
        self.defaultInstruction = {"rect": None, "scale": None}
        # current values
        self.instruction = dict(self.defaultInstruction)


class MapFrame(InstructionObject):
    """Class representing map (instructions maploc, scale, border)"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "map"
        # default values
        self.defaultInstruction = {
            "map": None,
            "mapType": None,
            "drawMap": True,
            "region": None,
            "rect": Rect2D(),
            "scaleType": 0,
            "scale": None,
            "center": None,
            "resolution": 300,
            "border": "y",
            "width": 1,
            "color": "0:0:0",
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = ""
        comment = ""

        # region settings
        region = gs.region(env=self.env)
        if self.instruction["scaleType"] == 0:  # match map
            map = self.instruction["map"]
            if self.instruction["mapType"] == "raster":
                comment = "# g.region raster=%s nsres=%s ewres=%s\n" % (
                    map,
                    region["nsres"],
                    region["ewres"],
                )
            else:
                comment = "# g.region vector=%s\n" % (map)
        elif self.instruction["scaleType"] == 1:  # saved region
            region = self.instruction["region"]
            comment = "# g.region region=%s\n" % region
        # current region, fixed scale
        elif self.instruction["scaleType"] in {2, 3}:
            comment = string.Template(
                "# g.region n=$n s=$s e=$e w=$w rows=$rows cols=$cols \n"
            ).substitute(**region)

        instr += comment
        instr += "\n"
        # maploc
        maplocInstruction = "maploc %.3f %.3f" % (
            self.instruction["rect"].x,
            self.instruction["rect"].y,
        )
        if self.instruction["scaleType"] != 3:
            maplocInstruction += "  %.3f %.3f" % (
                self.instruction["rect"].width,
                self.instruction["rect"].height,
            )
        instr += maplocInstruction
        instr += "\n"

        # scale
        if self.instruction["scaleType"] == 3:  # fixed scale
            scaleInstruction = "scale 1:%.0f" % (1 / self.instruction["scale"])
            instr += scaleInstruction
            instr += "\n"
        # border
        borderInstruction = ""
        if self.instruction["border"] == "n":
            borderInstruction = "border n"
        else:
            borderInstruction = "border y\n"
            borderInstruction += string.Template(
                "    width $width\n    color $color\n"
            ).substitute(self.instruction)
            borderInstruction += "    end"
        instr += borderInstruction
        instr += "\n"

        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        if "isRegionComment" in kwargs:
            isRegionComment = kwargs["isRegionComment"]
        instr = {}

        if instruction == "border":
            for line in text:
                if line.startswith("end"):
                    break
                try:
                    if line.split()[1].lower() in {"n", "no", "none"}:
                        instr["border"] = "n"
                        break
                    if line.split()[1].lower() in {"y", "yes"}:
                        instr["border"] = "y"
                    elif line.startswith("width"):
                        instr["width"] = line.split()[1]
                    elif line.startswith("color"):
                        instr["color"] = line.split()[1]
                except IndexError:
                    GError(_("Failed to read instruction %s") % instruction)
                    return False

        elif instruction == "scale":
            try:
                scaleText = text.strip("scale ").split(":")[1]
                # when scale instruction given and region comment also, then
                # scaletype is fixed scale
                if not isRegionComment:
                    instr["scaleType"] = 2
                else:
                    instr["scaleType"] = 3

                scale = 1 / float(scaleText)
                if abs(scale - self.instruction["scale"]) > (0.01 * scale):
                    GWarning(
                        _("Scale has changed, old value: %(old)s\nnew value: %(new)s")
                        % {"old": scale, "new": self.instruction["scale"]}
                    )
            except (ValueError, IndexError):
                GError(
                    _("Failed to read instruction %s.\nUse 1:25000 notation.")
                    % instruction
                )
                return False

        elif instruction == "maploc":
            maploc = text.strip("maploc ").split()
            if len(maploc) >= 2:
                if (
                    abs(self.instruction["rect"].Get()[0] - float(maploc[0])) > 0.5
                    or abs(self.instruction["rect"].Get()[1] - float(maploc[1])) > 0.5
                ):
                    GWarning(
                        _(
                            "Map frame position changed, old value: %(old1)s %(old2)s\n"
                            "new value: %(new1)s %(new2)s"
                        )
                        % {
                            "old1": maploc[0],
                            "old2": maploc[1],
                            "new1": self.instruction["rect"].Get()[0],
                            "new2": self.instruction["rect"].Get()[1],
                        }
                    )

                # instr["rect"] = wx.Rect2D(
                #     float(maploc[0]),
                #     float(maploc[1]),
                #     self.instruction["rect"][2],
                #     self.instruction["rect"][3],
                # )
            if len(maploc) == 4:
                if (
                    abs(self.instruction["rect"].Get()[2] - float(maploc[2])) > 0.5
                    or abs(self.instruction["rect"].Get()[3] - float(maploc[3])) > 0.5
                ):
                    GWarning(
                        _(
                            "Map frame size changed, old value: %(old1)s %(old2)s\n"
                            "new value: %(new1)s %(new2)s"
                        )
                        % {
                            "old1": maploc[2],
                            "old2": maploc[3],
                            "new1": self.instruction["rect"].Get()[2],
                            "new2": self.instruction["rect"].Get()[3],
                        }
                    )
                # instr['rect'] = wx.Rect2D(*map(float, maploc))
        self.instruction.update(instr)
        return True


class PageSetup(InstructionObject):
    """Class representing page instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "page"
        # default values
        self.defaultInstruction = {
            "Units": "inch",
            "Format": "a4",
            "Orientation": "Portrait",
            "Width": 8.268,
            "Height": 11.693,
            "Left": 0.5,
            "Right": 0.5,
            "Top": 1,
            "Bottom": 1,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        if self.instruction["Format"] == "custom":
            instr = string.Template(
                "paper\n    width $Width\n    height $Height\n"
            ).substitute(self.instruction)
        else:
            instr = string.Template("paper $Format\n").substitute(self.instruction)
        instr += string.Template(
            "    left $Left\n    right $Right\n    bottom $Bottom\n    top $Top\n"
            "    end"
        ).substitute(self.instruction)

        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        instr = {}
        self.cats = ["Width", "Height", "Left", "Right", "Top", "Bottom"]
        self.subInstr = dict(
            zip(
                ["width", "height", "left", "right", "top", "bottom"],
                self.cats,
                strict=True,
            )
        )

        if instruction == "paper":  # just for sure
            for line in text:
                if line.startswith("paper"):
                    if len(line.split()) > 1:
                        pformat = line.split()[1]
                        availableFormats = self._toDict(
                            gs.read_command("ps.map", flags="p", quiet=True)
                        )
                        # e.g. paper a3
                        try:
                            instr["Format"] = pformat
                            for key, value in availableFormats[pformat].items():
                                instr[key] = float(value)
                            break
                        except KeyError:
                            GError(
                                _(
                                    "Failed to read instruction %(file)s.\n"
                                    "Unknown format %(for)s"
                                )
                                % {"file": instruction, "for": format}
                            )
                            return False
                    else:
                        # paper
                        # width ...
                        instr["Format"] = "custom"
                # read subinstructions
                elif instr["Format"] == "custom" and not line.startswith("end"):
                    text = line.split()
                    try:
                        instr[self.subInstr[text[0]]] = float(text[1])
                    except (IndexError, KeyError):
                        GError(_("Failed to read instruction %s.") % instruction)
                        return False

            if "Orientation" in kwargs and kwargs["Orientation"] == "Landscape":
                instr["Width"], instr["Height"] = instr["Height"], instr["Width"]

            self.instruction.update(instr)
        return True

    def _toDict(self, paperStr):
        sizeDict = {}
        #     cats = self.subInstr[ 'Width', 'Height', 'Left', 'Right', 'Top', 'Bottom']
        for line in paperStr.strip().split("\n"):
            d = dict(zip(self.cats, line.split()[1:], strict=False))
            sizeDict[line.split()[0]] = d

        return sizeDict


class Mapinfo(InstructionObject):
    """Class representing mapinfo instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "mapinfo"
        # default values
        self.defaultInstruction = {
            "unit": "inch",
            "where": (0, 0),
            "font": "Helvetica",
            "fontsize": 10,
            "color": "0:0:0",
            "background": "none",
            "border": "none",
            "rect": None,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = "mapinfo\n"
        instr += "    where %.3f %.3f\n" % (
            self.instruction["where"][0],
            self.instruction["where"][1],
        )
        instr += string.Template(
            "    font $font\n    fontsize $fontsize\n    color $color\n"
        ).substitute(self.instruction)
        instr += string.Template(
            "    background $background\n    border $border\n"
        ).substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text):
        """Read instruction and save information"""
        instr = {}
        try:
            for line in text:
                sub = line.split(None, 1)
                if sub[0] == "font":
                    instr["font"] = sub[1]
                elif sub[0] == "fontsize":
                    instr["fontsize"] = int(sub[1])
                elif sub[0] == "color":
                    instr["color"] = sub[1]
                elif sub[0] == "background":
                    instr["background"] = sub[1]
                elif sub[0] == "border":
                    instr["border"] = sub[1]
                elif sub[0] == "where":
                    instr["where"] = float(sub[1].split()[0]), float(sub[1].split()[1])
        except (ValueError, IndexError):
            GError(_("Failed to read instruction %s") % instruction)
            return False
        self.instruction.update(instr)
        self.instruction["rect"] = self.EstimateRect(mapinfoDict=self.instruction)
        return True

    def EstimateRect(self, mapinfoDict):
        """Estimate size to draw mapinfo"""
        w = mapinfoDict["fontsize"] * 20  # any better estimation?
        h = mapinfoDict["fontsize"] * 7
        width = self.unitConv.convert(value=w, fromUnit="point", toUnit="inch")
        height = self.unitConv.convert(value=h, fromUnit="point", toUnit="inch")
        return Rect2D(
            x=float(mapinfoDict["where"][0]),
            y=float(mapinfoDict["where"][1]),
            width=width,
            height=height,
        )


class Text(InstructionObject):
    """Class representing text instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "text"
        # default values
        self.defaultInstruction = {
            "text": "",
            "font": "Helvetica",
            "fontsize": 10,
            "color": "black",
            "background": "none",
            "hcolor": "none",
            "hwidth": 1,
            "border": "none",
            "width": "1",
            "XY": True,
            "where": (0, 0),
            "unit": "inch",
            "rotate": None,
            "ref": "center center",
            "xoffset": 0,
            "yoffset": 0,
            "east": None,
            "north": None,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        text = self.instruction["text"].replace("\n", "\\n")
        instr = "text %s %s" % (self.instruction["east"], self.instruction["north"])
        instr += " %s\n" % text
        instr += string.Template(
            "    font $font\n    fontsize $fontsize\n    color $color\n"
        ).substitute(self.instruction)
        instr += string.Template("    hcolor $hcolor\n").substitute(self.instruction)
        if self.instruction["hcolor"] != "none":
            instr += string.Template("    hwidth $hwidth\n").substitute(
                self.instruction
            )
        instr += string.Template("    border $border\n").substitute(self.instruction)
        if self.instruction["border"] != "none":
            instr += string.Template("    width $width\n").substitute(self.instruction)
        instr += string.Template("    background $background\n").substitute(
            self.instruction
        )
        if self.instruction["ref"] != "0":
            instr += string.Template("    ref $ref\n").substitute(self.instruction)
        if self.instruction["rotate"]:
            instr += string.Template("    rotate $rotate\n").substitute(
                self.instruction
            )
        if float(self.instruction["xoffset"]) or float(self.instruction["yoffset"]):
            instr += string.Template(
                "    xoffset $xoffset\n    yoffset $yoffset\n"
            ).substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        map = kwargs["mapInstruction"]
        instr = {}
        for line in text:
            try:
                sub = line.split(None, 1)[0]
                if sub == "text":
                    e, n = line.split(None, 3)[1:3]
                    if "%" in e and "%" in n:
                        instr["XY"] = True
                        instr["east"], instr["north"] = self.PercentToReal(e, n)
                    else:
                        instr["XY"] = False
                        instr["east"], instr["north"] = float(e), float(n)

                    instr["text"] = line.split(None, 3)[3]

                elif sub == "font":
                    instr["font"] = line.split(None, 1)[1]
                elif sub == "fontsize":
                    instr["fontsize"] = float(line.split(None, 1)[1])
                elif sub == "color":
                    instr["color"] = line.split(None, 1)[1]
                elif sub == "width":
                    instr["width"] = line.split(None, 1)[1]
                elif sub == "hcolor":
                    instr["hcolor"] = line.split(None, 1)[1]
                elif sub == "hwidth":
                    instr["hwidth"] = line.split(None, 1)[1]
                elif sub == "background":
                    instr["background"] = line.split(None, 1)[1]
                elif sub == "border":
                    instr["border"] = line.split(None, 1)[1]
                elif sub == "ref":
                    instr["ref"] = line.split(None, 1)[1]
                elif sub == "rotate":
                    instr["rotate"] = float(line.split(None, 1)[1])
                elif sub == "xoffset":
                    instr["xoffset"] = int(line.split(None, 1)[1])
                elif sub == "yoffset":
                    instr["yoffset"] = int(line.split(None, 1)[1])
                elif sub == "opaque":
                    if line.split(None, 1)[1].lower() in {"n", "none"}:
                        instr["background"] = "none"

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False
        instr["where"] = PaperMapCoordinates(
            mapInstr=map,
            x=instr["east"],
            y=instr["north"],
            paperToMap=False,
            env=self.env,
        )
        self.instruction.update(instr)

        return True


class Image(InstructionObject):
    """Class representing eps instruction - image"""

    def __init__(self, id, settings, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.settings = settings
        self.type = "image"
        # default values
        self.defaultInstruction = {
            "epsfile": "",
            "XY": True,
            "where": (0, 0),
            "unit": "inch",
            "east": None,
            "north": None,
            "rotate": None,
            "scale": 1,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        self.ChangeRefPoint(toCenter=True)
        epsfile = self.instruction["epsfile"].replace(os.getenv("GISBASE"), "$GISBASE")

        instr = "eps %s %s\n" % (self.instruction["east"], self.instruction["north"])
        instr += "    epsfile %s\n" % epsfile
        if self.instruction["rotate"]:
            instr += string.Template("    rotate $rotate\n").substitute(
                self.instruction
            )
        if self.instruction["scale"]:
            instr += string.Template("    scale $scale\n").substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        mapInstr = kwargs["mapInstruction"]
        instr = {}
        for line in text:
            try:
                sub = line.split(None, 1)[0]
                if sub == "eps":
                    e, n = line.split(None, 3)[1:3]
                    if "%" in e and "%" in n:
                        instr["XY"] = True
                        instr["east"], instr["north"] = self.PercentToReal(e, n)
                    else:
                        instr["XY"] = False
                        instr["east"], instr["north"] = float(e), float(n)

                elif sub == "epsfile":
                    epsfile = line.split(None, 1)[1]
                    instr["epsfile"] = epsfile.replace("$GISBASE", os.getenv("GISBASE"))
                elif sub == "rotate":
                    instr["rotate"] = float(line.split(None, 1)[1])
                elif sub == "scale":
                    instr["scale"] = float(line.split(None, 1)[1])

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False
        if not Path(instr["epsfile"]).exists():
            GError(
                _("Failed to read instruction %(inst)s: file %(file)s not found.")
                % {"inst": instruction, "file": instr["epsfile"]}
            )
            return False

        instr["epsfile"] = os.path.abspath(instr["epsfile"])
        instr["size"] = self.GetImageOrigSize(instr["epsfile"])
        if "rotate" in instr:
            instr["size"] = BBoxAfterRotation(
                instr["size"][0], instr["size"][1], instr["rotate"]
            )
        self.instruction.update(instr)
        self.ChangeRefPoint(toCenter=False)
        instr["where"] = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east"],
            y=self.instruction["north"],
            paperToMap=False,
            env=self.env,
        )
        w = self.unitConv.convert(
            value=instr["size"][0], fromUnit="point", toUnit="inch"
        )
        h = self.unitConv.convert(
            value=instr["size"][1], fromUnit="point", toUnit="inch"
        )
        instr["rect"] = Rect2D(
            x=float(instr["where"][0]),
            y=float(instr["where"][1]),
            width=w * self.instruction["scale"],
            height=h * self.instruction["scale"],
        )
        self.instruction.update(instr)

        return True

    def ChangeRefPoint(self, toCenter):
        """Change reference point (left top x center)"""
        mapInstr = self.settings.FindInstructionByType("map")
        if not mapInstr:
            mapInstr = self.settings.FindInstructionByType("initMap")
        mapId = mapInstr.id
        if toCenter:
            center = self.instruction["rect"].GetCentre()
            ENCenter = PaperMapCoordinates(
                mapInstr=self.settings[mapId],
                x=center[0],
                y=center[1],
                paperToMap=True,
                env=self.env,
            )

            self.instruction["east"], self.instruction["north"] = ENCenter
        else:
            x, y = PaperMapCoordinates(
                mapInstr=self.settings[mapId],
                x=self.instruction["east"],
                y=self.instruction["north"],
                paperToMap=False,
                env=self.env,
            )
            w = self.unitConv.convert(
                value=self.instruction["size"][0], fromUnit="point", toUnit="inch"
            )
            h = self.unitConv.convert(
                value=self.instruction["size"][1], fromUnit="point", toUnit="inch"
            )
            x -= w * self.instruction["scale"] / 2
            y -= h * self.instruction["scale"] / 2
            e, n = PaperMapCoordinates(
                mapInstr=self.settings[mapId], x=x, y=y, paperToMap=True, env=self.env
            )
            self.instruction["east"], self.instruction["north"] = e, n

    def GetImageOrigSize(self, imagePath):
        """Get image size.

        If eps, size is read from image header.
        """
        fileName = os.path.split(imagePath)[1]
        if os.path.splitext(fileName)[1].lower() != ".eps":
            # we can use wx.Image
            img = wx.Image(fileName, type=wx.BITMAP_TYPE_ANY)
            return (img.GetWidth(), img.GetHeight())

        # if eps, read info from header
        bbInfo = "%%BoundingBox"
        w = h = 0
        with open(imagePath) as file:
            while file:
                line = file.readline()
                if line.find(bbInfo) == 0:
                    w, h = line.split()[3:5]
                    break
        return (float(w), float(h))


class NorthArrow(Image):
    """Class representing eps instruction -- North Arrow"""

    def __init__(self, id, settings, env):
        Image.__init__(self, id=id, settings=settings, env=env)
        self.type = "northArrow"

    def __str__(self):
        self.ChangeRefPoint(toCenter=True)
        epsfile = self.instruction["epsfile"].replace(os.getenv("GISBASE"), "$GISBASE")

        instr = "eps %s %s\n" % (self.instruction["east"], self.instruction["north"])
        instr += "# north arrow\n"
        instr += "    epsfile %s\n" % epsfile
        if self.instruction["rotate"]:
            instr += string.Template("    rotate $rotate\n").substitute(
                self.instruction
            )
        if self.instruction["scale"]:
            instr += string.Template("    scale $scale\n").substitute(self.instruction)
        instr += "    end"
        return instr


class Point(InstructionObject):
    """Class representing point instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "point"
        # default values
        self.defaultInstruction = {
            "symbol": os.path.join("basic", "x"),
            "color": "0:0:0",
            "fcolor": "200:200:200",
            "rotate": 0,
            "size": 10,
            "XY": True,
            "where": (0, 0),
            "unit": "inch",
            "east": None,
            "north": None,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = string.Template("point $east $north\n").substitute(self.instruction)
        instr += string.Template("    symbol $symbol\n").substitute(self.instruction)
        instr += string.Template("    color $color\n").substitute(self.instruction)
        instr += string.Template("    fcolor $fcolor\n").substitute(self.instruction)
        instr += string.Template("    rotate $rotate\n").substitute(self.instruction)
        instr += string.Template("    size $size\n").substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        mapInstr = kwargs["mapInstruction"]
        instr = {}
        for line in text:
            try:
                sub = line.split(None, 1)[0]
                if sub == "point":
                    e, n = line.split(None, 3)[1:3]
                    if "%" in e and "%" in n:
                        instr["XY"] = True
                        instr["east"], instr["north"] = self.PercentToReal(e, n)
                    else:
                        instr["XY"] = False
                        instr["east"], instr["north"] = float(e), float(n)

                elif sub == "symbol":
                    instr["symbol"] = line.split(None, 1)[1]
                elif sub == "rotate":
                    instr["rotate"] = float(line.split(None, 1)[1])
                elif sub == "size":
                    instr["size"] = float(line.split(None, 1)[1])
                elif sub == "color":
                    instr["color"] = line.split(None, 1)[1]
                elif sub == "fcolor":
                    instr["fcolor"] = line.split(None, 1)[1]

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        self.instruction.update(instr)
        instr["where"] = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east"],
            y=self.instruction["north"],
            paperToMap=False,
            env=self.env,
        )
        w = h = self.unitConv.convert(
            value=instr["size"], fromUnit="point", toUnit="inch"
        )
        instr["rect"] = Rect2D(
            x=float(instr["where"][0]) - w / 2,
            y=float(instr["where"][1] - h / 2),
            width=w,
            height=h,
        )
        self.instruction.update(instr)

        return True


class Line(InstructionObject):
    """Class representing line instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "line"
        # default values
        self.defaultInstruction = {
            "color": "0:0:0",
            "width": 2,
            "where": [wx.Point2D(), wx.Point2D()],
            "east1": None,
            "north1": None,
            "east2": None,
            "north2": None,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = string.Template("line $east1 $north1 $east2 $north2\n").substitute(
            self.instruction
        )
        instr += string.Template("    color $color\n").substitute(self.instruction)
        instr += string.Template("    width $width\n").substitute(self.instruction)
        instr += "    end\n"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        mapInstr = kwargs["mapInstruction"]
        instr = {}
        for line in text:
            try:
                sub = line.split(None, 1)[0]
                if sub == "line":
                    e1, n1, e2, n2 = line.split(None, 5)[1:5]
                    if "%" in e1 and "%" in n1 and "%" in e2 and "%" in n2:
                        instr["east1"], instr["north1"] = self.PercentToReal(e1, n1)
                        instr["east2"], instr["north2"] = self.PercentToReal(e2, n2)
                    else:
                        instr["east1"], instr["north1"] = float(e1), float(n1)
                        instr["east2"], instr["north2"] = float(e2), float(n2)

                elif sub == "width":
                    instr["width"] = float(line.split(None, 1)[1])
                elif sub == "color":
                    instr["color"] = line.split(None, 1)[1]

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        self.instruction.update(instr)
        e1, n1 = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east1"],
            y=self.instruction["north1"],
            paperToMap=False,
            env=self.env,
        )
        e2, n2 = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east2"],
            y=self.instruction["north2"],
            paperToMap=False,
            env=self.env,
        )
        instr["where"] = [wx.Point2D(e1, n1), wx.Point2D(e2, n2)]
        instr["rect"] = Rect2DPP(instr["where"][0], instr["where"][1])
        self.instruction.update(instr)

        return True


class Rectangle(InstructionObject):
    """Class representing rectangle instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "rectangle"
        # default values
        self.defaultInstruction = {
            "color": "0:0:0",
            "fcolor": "none",
            "width": 2,
            "east1": None,
            "north1": None,
            "east2": None,
            "north2": None,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = string.Template("rectangle $east1 $north1 $east2 $north2\n").substitute(
            self.instruction
        )
        instr += string.Template("    color $color\n").substitute(self.instruction)
        instr += string.Template("    fcolor $fcolor\n").substitute(self.instruction)
        instr += string.Template("    width $width\n").substitute(self.instruction)
        instr += "    end\n"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        mapInstr = kwargs["mapInstruction"]
        instr = {}
        for line in text:
            try:
                sub = line.split(None, 1)[0]
                if sub == "rectangle":
                    e1, n1, e2, n2 = line.split(None, 5)[1:5]
                    if "%" in e1 and "%" in n1 and "%" in e2 and "%" in n2:
                        instr["east1"], instr["north1"] = self.PercentToReal(e1, n1)
                        instr["east2"], instr["north2"] = self.PercentToReal(e2, n2)
                    else:
                        instr["east1"], instr["north1"] = float(e1), float(n1)
                        instr["east2"], instr["north2"] = float(e2), float(n2)

                elif sub == "width":
                    instr["width"] = float(line.split(None, 1)[1])
                elif sub == "color":
                    instr["color"] = line.split(None, 1)[1]
                elif sub == "fcolor":
                    instr["fcolor"] = line.split(None, 1)[1]

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        self.instruction.update(instr)
        e1, n1 = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east1"],
            y=self.instruction["north1"],
            paperToMap=False,
            env=self.env,
        )
        e2, n2 = PaperMapCoordinates(
            mapInstr=mapInstr,
            x=self.instruction["east2"],
            y=self.instruction["north2"],
            paperToMap=False,
            env=self.env,
        )
        instr["rect"] = Rect2DPP(wx.Point2D(e1, n1), wx.Point2D(e2, n2))
        self.instruction.update(instr)

        return True


class Scalebar(InstructionObject):
    """Class representing scalebar instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "scalebar"
        # default values
        self.defaultInstruction = {
            "unit": "inch",
            "where": (1, 1),
            "unitsLength": "auto",
            "unitsHeight": "inch",
            "length": None,
            "height": 0.1,
            "rect": None,
            "fontsize": 10,
            "background": "y",
            "scalebar": "f",
            "segment": 4,
            "numbers": 1,
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = string.Template("scalebar $scalebar\n").substitute(self.instruction)
        instr += "    where %.3f %.3f\n" % (
            self.instruction["where"][0],
            self.instruction["where"][1],
        )
        instr += string.Template(
            "    length $length\n    units $unitsLength\n"
        ).substitute(self.instruction)
        instr += string.Template("    height $height\n").substitute(self.instruction)
        instr += string.Template(
            "    segment $segment\n    numbers $numbers\n"
        ).substitute(self.instruction)
        instr += string.Template(
            "    fontsize $fontsize\n    background $background\n"
        ).substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        scale = kwargs["scale"]
        instr = {}
        for line in text:
            try:
                if line.startswith("scalebar"):
                    if "scalebar s" in line:
                        instr["scalebar"] = "s"
                    else:
                        instr["scalebar"] = "f"
                elif line.startswith("where"):
                    instr["where"] = list(map(float, line.split()[1:3]))
                elif line.startswith("length"):
                    instr["length"] = float(line.split()[1])
                elif line.startswith("units"):
                    if line.split()[1] in {
                        "auto",
                        "meters",
                        "kilometers",
                        "feet",
                        "miles",
                        "nautmiles",
                    }:
                        instr["unitsLength"] = line.split()[1]
                elif line.startswith("height"):
                    instr["height"] = float(line.split()[1])
                elif line.startswith("fontsize"):
                    instr["fontsize"] = float(line.split()[1])
                elif line.startswith("numbers"):
                    instr["numbers"] = int(line.split()[1])
                elif line.startswith("segment"):
                    instr["segment"] = int(line.split()[1])
                elif line.startswith("background"):
                    if line.split()[1].strip().lower() in {"y", "yes"}:
                        instr["background"] = "y"
                    elif line.split()[1].strip().lower() in {"n", "no", "none"}:
                        instr["background"] = "n"
            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        self.instruction.update(instr)
        w, h = self.EstimateSize(scalebarDict=self.instruction, scale=scale)
        x = self.instruction["where"][0] - w / 2
        y = self.instruction["where"][1] - h / 2
        self.instruction["rect"] = Rect2D(x, y, w, h)
        return True

    def EstimateSize(self, scalebarDict, scale):
        """Estimate size to draw scalebar"""
        units = projInfo()["units"]
        if not units or units not in self.unitConv.getAllUnits():
            units = "meters"
        if scalebarDict["unitsLength"] != "auto":
            length = self.unitConv.convert(
                value=scalebarDict["length"],
                fromUnit=scalebarDict["unitsLength"],
                toUnit="inch",
            )
        else:
            length = self.unitConv.convert(
                value=scalebarDict["length"], fromUnit=units, toUnit="inch"
            )

        length *= scale
        length *= 1.1  # for numbers on the edge
        height = scalebarDict["height"] + 2 * self.unitConv.convert(
            value=scalebarDict["fontsize"], fromUnit="point", toUnit="inch"
        )
        return (length, height)


class RasterLegend(InstructionObject):
    """Class representing colortable instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "rasterLegend"
        # default values
        self.defaultInstruction = {
            "rLegend": False,
            "unit": "inch",
            "rasterDefault": True,
            "raster": None,
            "discrete": None,
            "type": None,
            "where": (0, 0),
            "width": None,
            "height": None,
            "cols": 1,
            "font": "Helvetica",
            "fontsize": 10,
            "color": "black",
            "tickbar": "n",
            "range": False,
            "min": 0,
            "max": 0,
            "nodata": "n",
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = "colortable y\n"
        instr += string.Template("    raster $raster\n").substitute(self.instruction)
        instr += "    where %.3f %.3f\n" % (
            self.instruction["where"][0],
            self.instruction["where"][1],
        )
        if self.instruction["width"]:
            instr += string.Template("    width $width\n").substitute(self.instruction)
        instr += string.Template("    discrete $discrete\n").substitute(
            self.instruction
        )
        if self.instruction["discrete"] == "n":
            if self.instruction["height"]:
                instr += string.Template("    height $height\n").substitute(
                    self.instruction
                )
            instr += string.Template("    tickbar $tickbar\n").substitute(
                self.instruction
            )
            if self.instruction["range"]:
                instr += string.Template("    range $min $max\n").substitute(
                    self.instruction
                )
        else:
            instr += string.Template("    cols $cols\n").substitute(self.instruction)
            instr += string.Template("    nodata $nodata\n").substitute(
                self.instruction
            )
        instr += string.Template(
            "    font $font\n    fontsize $fontsize\n    color $color\n"
        ).substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        instr = {"rLegend": True}
        for line in text:
            try:
                if line.startswith("where"):
                    instr["where"] = list(map(float, line.split()[1:3]))
                elif line.startswith("font "):
                    instr["font"] = line.split()[1]
                elif line.startswith("fontsize"):
                    instr["fontsize"] = float(line.split()[1])
                elif line.startswith("color "):
                    instr["color"] = line.split()[1]
                elif line.startswith("raster"):
                    instr["raster"] = line.split()[1]
                elif line.startswith("width"):
                    instr["width"] = float(line.split()[1])
                elif line.startswith("height"):
                    instr["height"] = float(line.split()[1])
                elif line.startswith("cols"):
                    instr["cols"] = int(line.split()[1])
                elif line.startswith("range"):
                    instr["range"] = True
                    instr["min"] = float(line.split()[1])
                    instr["max"] = float(line.split()[2])
                elif line.startswith("nodata"):
                    if line.split()[1].strip().lower() in {"y", "yes"}:
                        instr["nodata"] = "y"
                    elif line.split()[1].strip().lower() in {"n", "no", "none"}:
                        instr["nodata"] = "n"
                elif line.startswith("tickbar"):
                    if line.split()[1].strip().lower() in {"y", "yes"}:
                        instr["tickbar"] = "y"
                    elif line.split()[1].strip().lower() in {"n", "no", "none"}:
                        instr["tickbar"] = "n"
                elif line.startswith("discrete"):
                    if line.split()[1].strip().lower() in {"y", "yes"}:
                        instr["discrete"] = "y"
                    elif line.split()[1].strip().lower() in {"n", "no", "none"}:
                        instr["discrete"] = "n"

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        if "raster" in instr:
            instr["rasterDefault"] = False
            if "discrete" not in instr:
                rasterType = getRasterType(map=instr["raster"])
                instr["type"] = rasterType
                if rasterType == "CELL":
                    instr["discrete"] = "y"
                else:
                    instr["discrete"] = "n"

        else:
            instr["rasterDefault"] = True
        self.instruction.update(instr)
        # add 'rect' in the end

        return True

    def EstimateHeight(self, raster, discrete, fontsize, cols=None, height=None):
        """Estimate height to draw raster legend"""
        if discrete == "n":
            if not height:
                height = self.unitConv.convert(
                    value=fontsize * 10, fromUnit="point", toUnit="inch"
                )

        if discrete == "y":
            if not cols:
                cols = 1

            rinfo = gs.raster_info(raster)
            if rinfo["datatype"] in {"DCELL", "FCELL"}:
                maxim = rinfo["max"]
                rows = ceil(maxim / cols)
            else:
                cat = (
                    gs.read_command("r.category", map=raster, sep=":")
                    .strip()
                    .split("\n")
                )
                rows = ceil(float(len(cat)) / cols)

            height = self.unitConv.convert(
                value=1.5 * rows * fontsize, fromUnit="point", toUnit="inch"
            )

        return height

    def EstimateWidth(
        self, raster, discrete, fontsize, cols=None, width=None, paperInstr=None
    ):
        """Estimate size to draw raster legend"""

        if discrete == "n":
            rinfo = gs.raster_info(raster)
            minim, maxim = rinfo["min"], rinfo["max"]
            if not width:
                width = self.unitConv.convert(
                    value=fontsize * 2, fromUnit="point", toUnit="inch"
                )
            text = len(max(str(minim), str(maxim), key=len))
            textPart = self.unitConv.convert(
                value=text * fontsize / 2, fromUnit="point", toUnit="inch"
            )
            width += textPart

        elif discrete == "y":
            if not cols:
                cols = 1

            if not width:
                paperWidth = (
                    paperInstr["Width"] - paperInstr["Right"] - paperInstr["Left"]
                )
                width = (paperWidth / cols) * (cols - 1) + 1

        return width


class VectorLegend(InstructionObject):
    """Class representing colortable instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "vectorLegend"
        # default values
        self.defaultInstruction = {
            "vLegend": False,
            "unit": "inch",
            "where": (0, 0),
            "defaultSize": True,
            "width": 0.4,
            "cols": 1,
            "span": None,
            "font": "Helvetica",
            "fontsize": 10,
            "border": "none",
        }
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = "vlegend\n"
        instr += "    where %.3f %.3f\n" % (
            self.instruction["where"][0],
            self.instruction["where"][1],
        )
        instr += string.Template("    font $font\n    fontsize $fontsize\n").substitute(
            self.instruction
        )
        instr += string.Template("    width $width\n    cols $cols\n").substitute(
            self.instruction
        )
        if self.instruction["span"]:
            instr += string.Template("    span $span\n").substitute(self.instruction)
        instr += string.Template("    border $border\n").substitute(self.instruction)
        instr += "    end"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        instr = {"vLegend": True}
        for line in text:
            try:
                if line.startswith("where"):
                    instr["where"] = list(map(float, line.split()[1:3]))
                elif line.startswith("font "):
                    instr["font"] = line.split()[1]
                elif line.startswith("fontsize"):
                    instr["fontsize"] = float(line.split()[1])
                elif line.startswith("width"):
                    instr["width"] = float(line.split()[1])
                elif line.startswith("cols"):
                    instr["cols"] = int(line.split()[1])
                elif line.startswith("span"):
                    instr["span"] = float(line.split()[1])
                elif line.startswith("border"):
                    instr["border"] = line.split()[1]

            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        self.instruction.update(instr)

        return True

    def EstimateSize(self, vectorInstr, fontsize, width=None, cols=None):
        """Estimate size to draw vector legend"""
        if not width:
            width = fontsize / 24.0

        if not cols:
            cols = 1

        vectors = vectorInstr["list"]
        labels = [vector[4] for vector in vectors if vector[3] != 0]
        extent = (len(max(labels, key=len)) * fontsize / 2, fontsize)
        wExtent = self.unitConv.convert(
            value=extent[0], fromUnit="point", toUnit="inch"
        )
        hExtent = self.unitConv.convert(
            value=extent[1], fromUnit="point", toUnit="inch"
        )
        w = (width + wExtent) * cols
        h = len(labels) * hExtent / cols
        h *= 1.1
        return (w, h)


class Raster(InstructionObject):
    """Class representing raster instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "raster"
        # default values
        self.defaultInstruction = {"isRaster": False, "raster": None}
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        return string.Template("raster $raster").substitute(self.instruction)

    def Read(self, instruction, text):
        """Read instruction and save information"""
        instr = {"isRaster": True}
        try:
            map = text.split()[1]
        except IndexError:
            GError(_("Failed to read instruction %s") % instruction)
            return False
        try:
            info = gs.find_file(map, element="cell")
        except ScriptError as e:
            GError(message=e.value)
            return False
        instr["raster"] = info["fullname"]

        self.instruction.update(instr)
        return True


class Vector(InstructionObject):
    """Class keeps vector layers"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "vector"
        # default values
        self.defaultInstruction = {"list": None}  # [vmap, type, id, lpos, label]
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        return ""

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        instr = {}

        for line in text:
            if not line.startswith(("vpoints", "vlines", "vareas")):
                continue
            # subtype
            if line.startswith("vpoints"):
                subType = "points"
            elif line.startswith("vlines"):
                subType = "lines"
            elif line.startswith("vareas"):
                subType = "areas"
            # name of vector map
            vmap = line.split()[1]
            try:
                info = gs.find_file(vmap, element="vector")
            except ScriptError as e:
                GError(message=e.value)
                return False
            vmap = info["fullname"]
            # id
            id = kwargs["id"]
            # lpos
            lpos = kwargs["vectorMapNumber"]
            # label
            label = "(".join(vmap.split("@")) + ")"
            break
        instr = [vmap, subType, id, lpos, label]
        if not self.instruction["list"]:
            self.instruction["list"] = []
        self.instruction["list"].append(instr)

        return True


class VProperties(InstructionObject):
    """Class represents instructions vareas, vlines, vpoints"""

    def __init__(self, id, subType, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "vProperties"
        self.subType = subType
        # default values
        if self.subType == "points":
            dd = {
                "subType": "points",
                "name": None,
                "type": "point or centroid",
                "connection": False,
                "layer": "1",
                "masked": "n",
                "color": "0:0:0",
                "width": 1,
                "fcolor": "255:0:0",
                "rgbcolumn": None,
                "symbol": os.path.join("basic", "x"),
                "eps": None,
                "size": 5,
                "sizecolumn": None,
                "scale": None,
                "rotation": False,
                "rotate": 0,
                "rotatecolumn": None,
                "label": None,
                "lpos": None,
            }
        elif self.subType == "lines":
            dd = {
                "subType": "lines",
                "name": None,
                "type": "line or boundary",
                "connection": False,
                "layer": "1",
                "masked": "n",
                "color": "0:0:0",
                "hwidth": 1,
                "hcolor": "none",
                "rgbcolumn": None,
                "width": 1,
                "cwidth": None,
                "style": "solid",
                "linecap": "butt",
                "label": None,
                "lpos": None,
            }
        else:  # areas
            dd = {
                "subType": "areas",
                "name": None,
                "connection": False,
                "layer": "1",
                "masked": "n",
                "color": "0:0:0",
                "width": 1,
                "fcolor": "none",
                "rgbcolumn": None,
                "pat": None,
                "pwidth": 1,
                "scale": 1,
                "label": None,
                "lpos": None,
            }
        self.defaultInstruction = dd
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        dic = self.instruction
        vInstruction = string.Template("v$subType $name\n").substitute(dic)
        # data selection
        if self.subType in {"points", "lines"}:
            vInstruction += string.Template("    type $type\n").substitute(dic)
        if dic["connection"]:
            vInstruction += string.Template("    layer $layer\n").substitute(dic)
            if "cats" in dic:
                vInstruction += string.Template("    cats $cats\n").substitute(dic)
            elif "where" in dic:
                vInstruction += string.Template("    where $where\n").substitute(dic)
        vInstruction += string.Template("    masked $masked\n").substitute(dic)
        # colors
        vInstruction += string.Template("    color $color\n").substitute(dic)
        if self.subType in {"points", "areas"}:
            if dic["color"] != "none":
                vInstruction += string.Template("    width $width\n").substitute(dic)
            if dic["rgbcolumn"]:
                vInstruction += string.Template(
                    "    rgbcolumn $rgbcolumn\n"
                ).substitute(dic)
            vInstruction += string.Template("    fcolor $fcolor\n").substitute(dic)
        else:  # noqa: PLR5501
            if dic["rgbcolumn"]:
                vInstruction += string.Template(
                    "    rgbcolumn $rgbcolumn\n"
                ).substitute(dic)
            elif dic["hcolor"] != "none":
                vInstruction += string.Template("    hwidth $hwidth\n").substitute(dic)
                vInstruction += string.Template("    hcolor $hcolor\n").substitute(dic)

        # size and style
        if self.subType == "points":
            if not dic["eps"]:
                vInstruction += string.Template("    symbol $symbol\n").substitute(dic)
            else:  # eps
                vInstruction += string.Template("    eps $eps\n").substitute(dic)
            if dic["size"]:
                vInstruction += string.Template("    size $size\n").substitute(dic)
            else:  # sizecolumn
                vInstruction += string.Template(
                    "    sizecolumn $sizecolumn\n"
                ).substitute(dic)
                vInstruction += string.Template("    scale $scale\n").substitute(dic)
            if dic["rotation"]:
                if dic["rotate"] is not None:
                    vInstruction += string.Template("    rotate $rotate\n").substitute(
                        dic
                    )
                else:
                    vInstruction += string.Template(
                        "    rotatecolumn $rotatecolumn\n"
                    ).substitute(dic)

        if self.subType == "areas":
            if dic["pat"] is not None:
                patternFile = dic["pat"].replace(os.getenv("GISBASE"), "$GISBASE")
                vInstruction += "    pat %s\n" % patternFile
                vInstruction += string.Template("    pwidth $pwidth\n").substitute(dic)
                vInstruction += string.Template("    scale $scale\n").substitute(dic)

        if self.subType == "lines":
            if dic["width"] is not None:
                vInstruction += string.Template("    width $width\n").substitute(dic)
            else:
                vInstruction += string.Template("    cwidth $cwidth\n").substitute(dic)
            vInstruction += string.Template("    style $style\n").substitute(dic)
            vInstruction += string.Template("    linecap $linecap\n").substitute(dic)
        # position and label in vlegend
        vInstruction += string.Template(
            "    label $label\n    lpos $lpos\n"
        ).substitute(dic)

        vInstruction += "    end"
        return vInstruction

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        instr = {}
        try:
            info = gs.find_file(name=text[0].split()[1], element="vector")
        except ScriptError as e:
            GError(message=e.value)
            return False
        instr["name"] = info["fullname"]
        # connection
        instr["connection"] = True
        self.mapDBInfo = VectorDBInfo(instr["name"])
        self.layers = self.mapDBInfo.layers.keys()
        if not self.layers:
            instr["connection"] = False

        # points
        if text[0].startswith("vpoints"):
            for line in text[1:]:
                if line.startswith("type"):
                    tp = []
                    if line.find("point") != -1:
                        tp.append("point")
                    if line.find("centroid") != -1:
                        tp.append("centroid")
                    instr["type"] = " or ".join(tp)
                elif line.startswith("fcolor"):
                    instr["fcolor"] = line.split()[1]
                elif line.startswith("rgbcolumn"):
                    instr["rgbcolumn"] = line.split()[1]
                elif line.startswith("symbol"):
                    instr["symbol"] = line.split()[1]
                elif line.startswith("eps"):
                    instr["eps"] = line.split()[1]
                elif line.startswith("size "):
                    instr["size"] = line.split()[1]
                elif line.startswith("sizecolumn"):
                    instr["size"] = None
                    instr["sizecolumn"] = line.split()[1]
                elif line.startswith("scale "):
                    instr["scale"] = float(line.split()[1])
                elif line.startswith("rotate "):
                    instr["rotation"] = True
                    instr["rotate"] = line.split()[1]
                elif line.startswith("rotatecolumn"):
                    instr["rotatecolumn"] = line.split()[1]
                    instr["rotation"] = True
                    instr["rotate"] = None

        # lines
        elif text[0].startswith("vlines"):
            for line in text[1:]:
                if line.startswith("type"):
                    tp = []
                    if line.find("line") != -1:
                        tp.append("line")
                    if line.find("boundary") != -1:
                        tp.append("boundary")
                    instr["type"] = " or ".join(tp)
                elif line.startswith("hwidth"):
                    instr["hwidth"] = float(line.split()[1])
                elif line.startswith("hcolor"):
                    instr["hcolor"] = line.split()[1]
                elif line.startswith("rgbcolumn"):
                    instr["rgbcolumn"] = line.split()[1]
                elif line.startswith("cwidth"):
                    instr["cwidth"] = float(line.split()[1])
                    instr["width"] = None
                elif line.startswith("style"):
                    instr["style"] = line.split()[1]
                elif line.startswith("linecap"):
                    instr["linecap"] = line.split()[1]

        elif text[0].startswith("vareas"):
            for line in text[1:]:
                if line.startswith("fcolor"):
                    instr["fcolor"] = line.split()[1]
                elif line.startswith("pat"):
                    patternFile = line.split()[1]
                    instr["pat"] = patternFile.replace("$GISBASE", os.getenv("GISBASE"))
                elif line.startswith("pwidth"):
                    instr["pwidth"] = float(line.split()[1])
                elif line.startswith("scale"):
                    instr["scale"] = float(line.split()[1])

        # same properties for all
        for line in text[1:]:
            if line.startswith("lpos"):
                instr["lpos"] = int(line.split()[1])
            elif line.startswith("label"):
                instr["label"] = line.split(None, 1)[1]
            elif line.startswith("layer"):
                instr["layer"] = line.split()[1]
            elif line.startswith("masked"):
                if line.split()[1].lower() in {"y", "yes"}:
                    instr["masked"] = "y"
                else:
                    instr["masked"] = "n"
            elif line.startswith("color"):
                instr["color"] = line.split()[1]
            elif line.startswith("rgbcolumn"):
                instr["rgbcolumn"] = line.split()[1]
            elif line.startswith("width"):
                instr["width"] = float(line.split()[1])

        if "label" not in instr:
            instr["label"] = "(".join(instr["name"].split("@")) + ")"
        if "lpos" not in instr:
            instr["lpos"] = kwargs["vectorMapNumber"]
        self.instruction.update(instr)

        return True


class Labels(InstructionObject):
    """Class representing labels instruction"""

    def __init__(self, id, env):
        InstructionObject.__init__(self, id=id, env=env)
        self.type = "labels"
        # default values
        self.defaultInstruction = {"labels": []}
        # current values
        self.instruction = dict(self.defaultInstruction)

    def __str__(self):
        instr = ""
        for label in self.instruction["labels"]:
            instr += "labels %s\n" % label
            instr += "end\n"
        return instr

    def Read(self, instruction, text, **kwargs):
        """Read instruction and save information"""
        for line in text:
            try:
                if line.startswith("labels"):
                    labels = line.split(None, 1)[1]
                    self.instruction["labels"].append(labels)
            except (IndexError, ValueError):
                GError(_("Failed to read instruction %s") % instruction)
                return False

        return True
