# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on.  If not, see <http://www.gnu.org/licenses/>.

import re
import os
import bpy
import json
import time
import calendar
import isodate
import pystache
import webbrowser
import ifcopenshell.api
import ifcopenshell.util.date
import ifcopenshell.util.sequence
import blenderbim.bim.helper
import blenderbim.bim.module.sequence.helper as helper
from datetime import datetime
from datetime import timedelta
from dateutil import parser, relativedelta
from blenderbim.bim.ifc import IfcStore
from bpy_extras.io_utils import ImportHelper
from ifcopenshell.api.sequence.data import Data
from ifcopenshell.api.resource.data import Data as ResourceData


class AddWorkPlan(bpy.types.Operator):
    bl_idname = "bim.add_work_plan"
    bl_label = "Add Work Plan"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        ifcopenshell.api.run("sequence.add_work_plan", IfcStore.get_file())
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class EditWorkPlan(bpy.types.Operator):
    bl_idname = "bim.edit_work_plan"
    bl_options = {"REGISTER", "UNDO"}
    bl_label = "Edit Work Plan"

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkPlanProperties
        attributes = blenderbim.bim.helper.export_attributes(props.work_plan_attributes, self.export_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_work_plan",
            self.file,
            **{"work_plan": self.file.by_id(props.active_work_plan_id), "attributes": attributes},
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_work_plan()
        return {"FINISHED"}

    def export_attributes(self, attributes, prop):
        if "Date" in prop.name or "Time" in prop.name:
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_datetime(prop.string_value)
            return True
        elif prop.name == "Duration" or prop.name == "TotalFloat":
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_duration(prop.string_value)
            return True


class RemoveWorkPlan(bpy.types.Operator):
    bl_idname = "bim.remove_work_plan"
    bl_label = "Remove Work Plan"
    bl_options = {"REGISTER", "UNDO"}
    work_plan: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run("sequence.remove_work_plan", self.file, **{"work_plan": self.file.by_id(self.work_plan)})
        Data.load(self.file)
        return {"FINISHED"}


class EnableEditingWorkPlan(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_plan"
    bl_label = "Enable Editing Work Plan"
    bl_options = {"REGISTER", "UNDO"}
    work_plan: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkPlanProperties
        props.work_plan_attributes.clear()

        data = Data.work_plans[self.work_plan]

        blenderbim.bim.helper.import_attributes("IfcWorkPlan", props.work_plan_attributes, data, self.import_attributes)

        props.active_work_plan_id = self.work_plan
        props.editing_type = "ATTRIBUTES"
        return {"FINISHED"}

    def import_attributes(self, name, prop, data):
        if name in ["CreationDate", "StartTime", "FinishTime"]:
            prop.string_value = "" if prop.is_null else data[name].isoformat()
            return True


class DisableEditingWorkPlan(bpy.types.Operator):
    bl_idname = "bim.disable_editing_work_plan"
    bl_options = {"REGISTER", "UNDO"}
    bl_label = "Disable Editing Work Plan"

    def execute(self, context):
        context.scene.BIMWorkPlanProperties.active_work_plan_id = 0
        return {"FINISHED"}


class EnableEditingWorkPlanSchedules(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_plan_schedules"
    bl_label = "Enable Editing Work Plan Schedules"
    bl_options = {"REGISTER", "UNDO"}
    work_plan: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkPlanProperties
        props.active_work_plan_id = self.work_plan
        props.editing_type = "SCHEDULES"
        return {"FINISHED"}


class AssignWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.assign_work_schedule"
    bl_label = "Assign Work Schedule"
    bl_options = {"REGISTER", "UNDO"}
    work_plan: bpy.props.IntProperty()
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "aggregate.assign_object",
            self.file,
            **{
                "relating_object": self.file.by_id(self.work_plan),
                "product": self.file.by_id(self.work_schedule),
            },
        )
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class UnassignWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.unassign_work_schedule"
    bl_label = "Unassign Work Schedule"
    bl_options = {"REGISTER", "UNDO"}
    work_plan: bpy.props.IntProperty()
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "aggregate.unassign_object",
            self.file,
            **{
                "relating_object": self.file.by_id(self.work_plan),
                "product": self.file.by_id(self.work_schedule),
            },
        )
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class AddWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.add_work_schedule"
    bl_label = "Add Work Schedule"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        ifcopenshell.api.run("sequence.add_work_schedule", IfcStore.get_file())
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class EditWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.edit_work_schedule"
    bl_label = "Edit Work Schedule"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        attributes = blenderbim.bim.helper.export_attributes(props.work_schedule_attributes, self.export_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_work_schedule",
            self.file,
            **{"work_schedule": self.file.by_id(props.active_work_schedule_id), "attributes": attributes},
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_work_schedule()
        return {"FINISHED"}

    def export_attributes(self, attributes, prop):
        if "Date" in prop.name or "Time" in prop.name:
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_datetime(prop.string_value)
            return True
        elif prop.name == "Duration" or prop.name == "TotalFloat":
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_duration(prop.string_value)
            return True


class RemoveWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.remove_work_schedule"
    bl_label = "Remove Work Schedule"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.remove_work_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule)
        )
        Data.load(self.file)
        return {"FINISHED"}


class EnableEditingWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_schedule"
    bl_label = "Enable Editing Work Schedule"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.props.active_work_schedule_id = self.work_schedule
        self.props.work_schedule_attributes.clear()
        self.enable_editing_work_schedule()
        self.props.editing_type = "WORK_SCHEDULE"
        return {"FINISHED"}

    def enable_editing_work_schedule(self):
        data = Data.work_schedules[self.work_schedule]

        blenderbim.bim.helper.import_attributes(
            "IfcWorkSchedule", self.props.work_schedule_attributes, data, self.import_attributes
        )

    def import_attributes(self, name, prop, data):
        if name in ["CreationDate", "StartTime", "FinishTime"]:
            prop.string_value = "" if prop.is_null else data[name].isoformat()
            return True


class EnableEditingTasks(bpy.types.Operator):
    bl_idname = "bim.enable_editing_tasks"
    bl_label = "Enable Editing Tasks"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.tprops = context.scene.BIMTaskTreeProperties
        self.props.active_work_schedule_id = self.work_schedule
        self.tprops.tasks.clear()

        self.contracted_tasks = json.loads(self.props.contracted_tasks)
        self.sort_keys = {
            i: self.get_sort_key(Data.tasks[i]) for i in Data.work_schedules[self.work_schedule]["RelatedObjects"]
        }

        related_object_ids = sorted(self.sort_keys, key=self.natural_sort_key)
        if self.props.is_sort_reversed:
            related_object_ids.reverse()

        for related_object_id in related_object_ids:
            self.create_new_task_li(related_object_id, 0)
        bpy.ops.bim.load_task_properties()
        self.props.editing_type = "TASKS"
        return {"FINISHED"}

    def create_new_task_li(self, related_object_id, level_index):
        task = Data.tasks[related_object_id]
        new = self.tprops.tasks.add()
        new.ifc_definition_id = related_object_id
        new.is_expanded = related_object_id not in self.contracted_tasks
        new.level_index = level_index
        if task["RelatedObjects"]:
            new.has_children = True
            if new.is_expanded:
                self.sort_keys = {i: self.get_sort_key(Data.tasks[i]) for i in task["RelatedObjects"]}
                related_object_ids = sorted(self.sort_keys, key=self.natural_sort_key)
                if self.props.is_sort_reversed:
                    related_object_ids.reverse()
                for related_object_id in related_object_ids:
                    self.create_new_task_li(related_object_id, level_index + 1)

    def get_sort_key(self, task):
        # Sorting only applies to actual tasks, not the WBS
        if task["RelatedObjects"]:
            # Sorry for the hack
            return "0000000000" + (task["Identification"] or "")
        if not self.props.sort_column:
            return task["Identification"] or ""
        column_type, name = self.props.sort_column.split(".")
        if column_type == "IfcTask":
            return task.get(name)
        elif column_type == "IfcTaskTime" and task.get("TaskTime"):
            task_time = Data.task_times[task.get("TaskTime")].get(name)
        return task["Identification"] or ""

    def natural_sort_key(self, i, _nsre=re.compile("([0-9]+)")):
        s = self.sort_keys[i]
        return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]


class LoadTaskProperties(bpy.types.Operator):
    bl_idname = "bim.load_task_properties"
    bl_label = "Load Task Properties"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.props = context.scene.BIMWorkScheduleProperties
        self.tprops = context.scene.BIMTaskTreeProperties
        self.props.is_task_update_enabled = False
        for item in self.tprops.tasks:
            if self.task and item.ifc_definition_id != self.task:
                continue
            task = Data.tasks[item.ifc_definition_id]
            item.name = task["Name"] or "Unnamed"
            item.identification = task["Identification"] or "XXX"
            if self.props.active_task_id:
                item.is_predecessor = self.props.active_task_id in [
                    Data.sequences[r]["RelatedProcess"] for r in task["IsPredecessorTo"]
                ]
                item.is_successor = self.props.active_task_id in [
                    Data.sequences[r]["RelatingProcess"] for r in task["IsSuccessorFrom"]
                ]

            calendar = ifcopenshell.util.sequence.derive_calendar(self.file.by_id(item.ifc_definition_id))
            if calendar:
                calendar = Data.work_calendars[calendar.id()]
            if task["HasAssignmentsWorkCalendar"]:
                item.calendar = calendar["Name"] or "Unnamed"
            else:
                item.calendar = ""
                item.derived_calendar = calendar["Name"] or "Unnamed" if calendar else ""

            if task["TaskTime"]:
                task_time = Data.task_times[task["TaskTime"]]
                item.start = self.canonicalise_time(task_time["ScheduleStart"])
                item.finish = self.canonicalise_time(task_time["ScheduleFinish"])
                item.duration = (
                    isodate.duration_isoformat(task_time["ScheduleDuration"]) if task_time["ScheduleDuration"] else "-"
                )
            else:
                derived_start = helper.derive_date(item.ifc_definition_id, "ScheduleStart", is_earliest=True)
                derived_finish = helper.derive_date(item.ifc_definition_id, "ScheduleFinish", is_latest=True)
                item.derived_start = self.canonicalise_time(derived_start) if derived_start else ""
                item.derived_finish = self.canonicalise_time(derived_finish) if derived_finish else ""
                if derived_start and derived_finish and calendar:
                    derived_duration = ifcopenshell.util.sequence.count_working_days(
                        derived_start, derived_finish, self.file.by_id(calendar["id"])
                    )
                    item.derived_duration = f"P{derived_duration}D"
                item.start = "-"
                item.finish = "-"
                item.duration = "-"

        self.props.is_task_update_enabled = True
        return {"FINISHED"}

    def canonicalise_time(self, time):
        if not time:
            return "-"
        return time.strftime("%d/%m/%y")


class DisableEditingWorkSchedule(bpy.types.Operator):
    bl_idname = "bim.disable_editing_work_schedule"
    bl_label = "Disable Editing Work Schedule"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkScheduleProperties.active_work_schedule_id = 0
        return {"FINISHED"}


class AddTask(bpy.types.Operator):
    bl_idname = "bim.add_task"
    bl_label = "Add Task"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run("sequence.add_task", self.file, parent_task=self.file.by_id(self.task))
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}


class AddSummaryTask(bpy.types.Operator):
    bl_idname = "bim.add_summary_task"
    bl_label = "Add Task"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run("sequence.add_task", self.file, work_schedule=self.file.by_id(self.work_schedule))
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}


class ExpandTask(bpy.types.Operator):
    bl_idname = "bim.expand_task"
    bl_label = "Expand Task"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        contracted_tasks = json.loads(props.contracted_tasks)
        contracted_tasks.remove(self.task)
        props.contracted_tasks = json.dumps(contracted_tasks)
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}


class ContractTask(bpy.types.Operator):
    bl_idname = "bim.contract_task"
    bl_label = "Contract Task"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        contracted_tasks = json.loads(props.contracted_tasks)
        contracted_tasks.append(self.task)
        props.contracted_tasks = json.dumps(contracted_tasks)
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}


class RemoveTask(bpy.types.Operator):
    bl_idname = "bim.remove_task"
    bl_label = "Remove Task"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.remove_task",
            self.file,
            task=IfcStore.get_file().by_id(self.task),
        )
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}


class EnableEditingTaskTime(bpy.types.Operator):
    bl_idname = "bim.enable_editing_task_time"
    bl_label = "Enable Editing Task Time"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()

        task_time_id = Data.tasks[self.task]["TaskTime"] or self.add_task_time().id()

        props.task_time_attributes.clear()

        data = Data.task_times[task_time_id]

        blenderbim.bim.helper.import_attributes("IfcTaskTime", props.task_time_attributes, data, self.import_attributes)
        props.active_task_time_id = task_time_id
        props.active_task_id = self.task
        props.editing_task_type = "TASKTIME"
        return {"FINISHED"}

    def import_attributes(self, name, prop, data):
        if prop.data_type == "string":
            if isinstance(data[name], datetime):
                prop.string_value = "" if prop.is_null else data[name].isoformat()
                return True
            elif isinstance(data[name], isodate.Duration):
                prop.string_value = (
                    "" if prop.is_null else ifcopenshell.util.date.datetime2ifc(data[name], "IfcDuration")
                )
                return True

    def add_task_time(self):
        task_time = ifcopenshell.api.run("sequence.add_task_time", self.file, task=self.file.by_id(self.task))
        Data.load(self.file)
        return task_time


class EditTaskTime(bpy.types.Operator):
    bl_idname = "bim.edit_task_time"
    bl_label = "Edit Task Time"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        attributes = blenderbim.bim.helper.export_attributes(props.task_time_attributes, self.export_attributes)

        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_task_time",
            self.file,
            **{"task_time": self.file.by_id(props.active_task_time_id), "attributes": attributes},
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_task_time()
        bpy.ops.bim.load_task_properties(task=props.active_task_id)
        return {"FINISHED"}

    def export_attributes(self, attributes, prop):
        if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime":
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_datetime(prop.string_value)
            return True
        elif prop.name == "ScheduleDuration":
            if prop.is_null:
                attributes[prop.name] = None
                return True
            attributes[prop.name] = helper.parse_duration(prop.string_value)
            return True


class EnableEditingTask(bpy.types.Operator):
    bl_idname = "bim.enable_editing_task"
    bl_label = "Enable Editing Task"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        props.task_attributes.clear()
        data = Data.tasks[self.task]

        blenderbim.bim.helper.import_attributes("IfcTask", props.task_attributes, data)
        props.active_task_id = self.task
        props.editing_task_type = "ATTRIBUTES"
        return {"FINISHED"}


class DisableEditingTask(bpy.types.Operator):
    bl_idname = "bim.disable_editing_task"
    bl_label = "Disable Editing Task"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkScheduleProperties.active_task_id = 0
        context.scene.BIMWorkScheduleProperties.active_task_time_id = 0
        return {"FINISHED"}


class EditTask(bpy.types.Operator):
    bl_idname = "bim.edit_task"
    bl_label = "Edit Task"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        attributes = blenderbim.bim.helper.export_attributes(props.task_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_task", self.file, **{"task": self.file.by_id(props.active_task_id), "attributes": attributes}
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_task()
        bpy.ops.bim.load_task_properties(task=props.active_task_id)
        return {"FINISHED"}


class CopyTaskAttribute(bpy.types.Operator):
    bl_idname = "bim.copy_task_attribute"
    bl_label = "Copy Task Attribute"
    bl_options = {"REGISTER", "UNDO"}
    data: bpy.props.StringProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        data = json.loads(self.data)
        self.file = IfcStore.get_file()
        props = context.scene.BIMTaskTreeProperties
        for task in props.tasks:
            if task.is_selected:
                ifcopenshell.api.run(
                    "sequence.edit_task",
                    self.file,
                    **{
                        "task": self.file.by_id(task.ifc_definition_id),
                        "attributes": {data["name"]: None if data["is_null"] else data["value"]},
                    },
                )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class AssignPredecessor(bpy.types.Operator):
    bl_idname = "bim.assign_predecessor"
    bl_label = "Assign Predecessor"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        rel = ifcopenshell.api.run(
            "sequence.assign_sequence",
            self.file,
            relating_process=IfcStore.get_file().by_id(self.task),
            related_process=IfcStore.get_file().by_id(props.active_task_id),
        )
        Data.load(self.file)
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class AssignSuccessor(bpy.types.Operator):
    bl_idname = "bim.assign_successor"
    bl_label = "Assign Successor"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        rel = ifcopenshell.api.run(
            "sequence.assign_sequence",
            self.file,
            relating_process=IfcStore.get_file().by_id(props.active_task_id),
            related_process=IfcStore.get_file().by_id(self.task),
        )
        Data.load(self.file)
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class UnassignPredecessor(bpy.types.Operator):
    bl_idname = "bim.unassign_predecessor"
    bl_label = "Unassign Predecessor"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.unassign_sequence",
            self.file,
            relating_process=IfcStore.get_file().by_id(self.task),
            related_process=IfcStore.get_file().by_id(props.active_task_id),
        )
        Data.load(self.file)
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class UnassignSuccessor(bpy.types.Operator):
    bl_idname = "bim.unassign_successor"
    bl_label = "Unassign Successor"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.unassign_sequence",
            self.file,
            relating_process=self.file.by_id(props.active_task_id),
            related_process=self.file.by_id(self.task),
        )
        Data.load(self.file)
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class AssignProduct(bpy.types.Operator):
    bl_idname = "bim.assign_product"
    bl_label = "Assign Product"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()
    relating_product: bpy.props.StringProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        relating_products = (
            [bpy.data.objects.get(self.relating_product)] if self.relating_product else context.selected_objects
        )
        for relating_product in relating_products:
            if not relating_product.BIMObjectProperties.ifc_definition_id:
                continue
            ifcopenshell.api.run(
                "sequence.assign_product",
                self.file,
                relating_product=self.file.by_id(relating_product.BIMObjectProperties.ifc_definition_id),
                related_object=self.file.by_id(self.task),
            )
        Data.load(self.file)
        bpy.ops.bim.load_task_outputs()
        return {"FINISHED"}


class UnassignProduct(bpy.types.Operator):
    bl_idname = "bim.unassign_product"
    bl_label = "Unassign Product"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()
    relating_product: bpy.props.StringProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        relating_products = (
            [bpy.data.objects.get(self.relating_product)] if self.relating_product else context.selected_objects
        )
        for relating_product in relating_products:
            if not relating_product.BIMObjectProperties.ifc_definition_id:
                continue
            ifcopenshell.api.run(
                "sequence.unassign_product",
                self.file,
                relating_product=self.file.by_id(relating_product.BIMObjectProperties.ifc_definition_id),
                related_object=self.file.by_id(self.task),
            )
        Data.load(self.file)
        bpy.ops.bim.load_task_outputs()
        return {"FINISHED"}


class AssignProcess(bpy.types.Operator):
    bl_idname = "bim.assign_process"
    bl_label = "Assign Process"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()
    related_object_type: bpy.props.StringProperty()
    related_object: bpy.props.StringProperty()
    resource: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        if self.related_object_type == "RESOURCE":
            self.assign_resource()
        elif self.related_object_type == "PRODUCT":
            self.assign_product(context)
        elif self.related_object_type == "CONTROL":
            pass  # TODO
        return {"FINISHED"}

    def assign_resource(self):
        task = self.file.by_id(self.task)
        resource = self.file.by_id(self.resource)
        subresource = ifcopenshell.api.run(
            "resource.add_resource",
            self.file,
            **{"parent_resource": resource, "ifc_class": resource.is_a(), "name": resource.Name},
        )
        ifcopenshell.api.run(
            "sequence.assign_process", self.file, **{"related_object": subresource, "relating_process": task}
        )
        ResourceData.load(self.file)
        Data.load(self.file)
        bpy.ops.bim.load_resources()
        bpy.ops.bim.load_task_resources()

    def assign_product(self, context):
        task = self.file.by_id(self.task)
        related_objects = (
            [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
        )
        for related_object in related_objects:
            if not related_object.BIMObjectProperties.ifc_definition_id:
                continue
            ifcopenshell.api.run(
                "sequence.assign_process",
                self.file,
                related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id),
                relating_process=task,
            )
        Data.load(self.file)
        bpy.ops.bim.load_task_inputs()


class UnassignProcess(bpy.types.Operator):
    bl_idname = "bim.unassign_process"
    bl_label = "Unassign Process"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()
    related_object_type: bpy.props.StringProperty()
    related_object: bpy.props.StringProperty()
    resource: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        if self.related_object_type == "RESOURCE":
            self.unassign_resource()
        elif self.related_object_type == "PRODUCT":
            self.unassign_product(context)
        elif self.related_object_type == "CONTROL":
            pass  # TODO
        return {"FINISHED"}

    def unassign_resource(self):
        task = self.file.by_id(self.task)
        resource = self.file.by_id(self.resource)
        ifcopenshell.api.run(
            "sequence.unassign_process", self.file, related_object=resource, relating_process=task
        )
        ifcopenshell.api.run("resource.remove_resource", self.file, resource=resource)
        ResourceData.load(self.file)
        Data.load(self.file)
        bpy.ops.bim.load_resources()
        bpy.ops.bim.load_task_resources()

    def unassign_product(self, context):
        task = self.file.by_id(self.task)
        related_objects = (
            [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
        )
        for related_object in related_objects:
            if not related_object.BIMObjectProperties.ifc_definition_id:
                continue
            ifcopenshell.api.run(
                "sequence.unassign_process",
                self.file,
                related_object=self.file.by_id(related_object.BIMObjectProperties.ifc_definition_id),
                relating_process=task,
            )
        Data.load(self.file)
        bpy.ops.bim.load_task_inputs()


class GenerateGanttChart(bpy.types.Operator):
    bl_idname = "bim.generate_gantt_chart"
    bl_label = "Generate Gantt Chart"
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.json = []
        self.sequence_type_map = {
            None: "FS",
            "START_START": "SS",
            "START_FINISH": "SF",
            "FINISH_START": "FS",
            "FINISH_FINISH": "FF",
            "USERDEFINED": "FS",
            "NOTDEFINED": "FS",
        }
        for task_id in Data.work_schedules[self.work_schedule]["RelatedObjects"]:
            self.create_new_task_json(task_id)
        with open(os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
            with open(os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
                f.write(pystache.render(t.read(), {"json_data": json.dumps(self.json)}))
        webbrowser.open("file://" + os.path.join(context.scene.BIMProperties.data_dir, "gantt", "index.html"))
        return {"FINISHED"}

    def create_new_task_json(self, task_id):
        task = self.file.by_id(task_id)
        data = {
            "pID": task.id(),
            "pName": task.Name,
            "pStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
            "pEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
            "pPlanStart": task.TaskTime.ScheduleStart if task.TaskTime else "",
            "pPlanEnd": task.TaskTime.ScheduleFinish if task.TaskTime else "",
            "pMile": 1 if task.IsMilestone else 0,
            "pComp": 0,
            "pGroup": 1 if task.IsNestedBy else 0,
            "pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
            "pOpen": 1,
            "pCost": 1,
        }
        if task.TaskTime and task.TaskTime.IsCritical:
            data["pClass"] = "gtaskred"
        elif data["pGroup"]:
            data["pClass"] = "ggroupblack"
        elif data["pMile"]:
            data["pClass"] = "gmilestone"
        else:
            data["pClass"] = "gtaskblue"
        data["pDepend"] = ",".join(
            [
                "{}{}".format(rel.RelatingProcess.id(), self.sequence_type_map[rel.SequenceType])
                for rel in task.IsSuccessorFrom or []
            ]
        )
        self.json.append(data)
        for task_id in Data.tasks[task_id]["RelatedObjects"]:
            self.create_new_task_json(task_id)


class AddWorkCalendar(bpy.types.Operator):
    bl_idname = "bim.add_work_calendar"
    bl_label = "Add Work Calendar"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        ifcopenshell.api.run("sequence.add_work_calendar", IfcStore.get_file())
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class EditWorkCalendar(bpy.types.Operator):
    bl_idname = "bim.edit_work_calendar"
    bl_label = "Edit Work Calendar"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkCalendarProperties
        attributes = blenderbim.bim.helper.export_attributes(props.work_calendar_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_work_calendar",
            self.file,
            **{"work_calendar": self.file.by_id(props.active_work_calendar_id), "attributes": attributes},
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_work_calendar()
        return {"FINISHED"}


class RemoveWorkCalendar(bpy.types.Operator):
    bl_idname = "bim.remove_work_calendar"
    bl_label = "Remove Work Plan"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.remove_work_calendar", self.file, **{"work_calendar": self.file.by_id(self.work_calendar)}
        )
        Data.load(self.file)
        return {"FINISHED"}


class EnableEditingWorkCalendar(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_calendar"
    bl_label = "Enable Editing Work Calendar"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkCalendarProperties
        self.props.work_calendar_attributes.clear()

        data = Data.work_calendars[self.work_calendar]

        blenderbim.bim.helper.import_attributes("IfcWorkCalendar", self.props.work_calendar_attributes, data)
        self.props.active_work_calendar_id = self.work_calendar
        self.props.editing_type = "ATTRIBUTES"
        return {"FINISHED"}


class DisableEditingWorkCalendar(bpy.types.Operator):
    bl_idname = "bim.disable_editing_work_calendar"
    bl_label = "Disable Editing Work Calendar"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkCalendarProperties.active_work_calendar_id = 0
        return {"FINISHED"}


class ImportP6(bpy.types.Operator, ImportHelper):
    bl_idname = "import_p6.bim"
    bl_label = "Import P6"
    bl_options = {"REGISTER", "UNDO"}
    filename_ext = ".xml"
    filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})

    def execute(self, context):
        from ifc4d.p62ifc import P62Ifc

        self.file = IfcStore.get_file()
        start = time.time()
        p62ifc = P62Ifc()
        p62ifc.xml = self.filepath
        p62ifc.file = self.file
        p62ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] if self.file.by_type("IfcWorkPlan") else None
        p62ifc.execute()
        Data.load(IfcStore.get_file())
        print("Import finished in {:.2f} seconds".format(time.time() - start))
        return {"FINISHED"}


class ImportMSP(bpy.types.Operator, ImportHelper):
    bl_idname = "import_msp.bim"
    bl_label = "Import MSP"
    bl_options = {"REGISTER", "UNDO"}
    filename_ext = ".xml"
    filter_glob: bpy.props.StringProperty(default="*.xml", options={"HIDDEN"})

    def execute(self, context):
        from ifc4d.msp2ifc import MSP2Ifc

        self.file = IfcStore.get_file()
        start = time.time()
        msp2ifc = MSP2Ifc()
        msp2ifc.xml = self.filepath
        msp2ifc.file = self.file
        msp2ifc.work_plan = self.file.by_type("IfcWorkPlan")[0] if self.file.by_type("IfcWorkPlan") else None
        msp2ifc.execute()
        Data.load(IfcStore.get_file())
        print("Import finished in {:.2f} seconds".format(time.time() - start))
        return {"FINISHED"}


class EnableEditingWorkCalendarTimes(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_calendar_times"
    bl_label = "Enable Editing Work Calendar Times"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkCalendarProperties
        props.active_work_calendar_id = self.work_calendar
        props.editing_type = "WORKTIMES"
        return {"FINISHED"}


class AddWorkTime(bpy.types.Operator):
    bl_idname = "bim.add_work_time"
    bl_label = "Add Work Time"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()
    time_type: bpy.props.StringProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.add_work_time",
            self.file,
            **{"work_calendar": self.file.by_id(self.work_calendar), "time_type": self.time_type},
        )
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class EnableEditingWorkTime(bpy.types.Operator):
    bl_idname = "bim.enable_editing_work_time"
    bl_label = "Enable Editing Work Time"
    bl_options = {"REGISTER", "UNDO"}
    work_time: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkCalendarProperties
        self.props.work_time_attributes.clear()

        data = Data.work_times[self.work_time]

        blenderbim.bim.helper.import_attributes(
            "IfcWorkTime", self.props.work_time_attributes, data, self.import_attributes
        )

        self.initialise_recurrence_components()
        self.load_recurrence_pattern_data(data)
        self.props.active_work_time_id = self.work_time
        return {"FINISHED"}

    def import_attributes(self, name, prop, data):
        if name in ["Start", "Finish"]:
            prop.string_value = "" if prop.is_null else data[name].isoformat()
            return True

    def initialise_recurrence_components(self):
        if len(self.props.day_components) == 0:
            for i in range(0, 31):
                new = self.props.day_components.add()
                new.name = str(i + 1)
        if len(self.props.weekday_components) == 0:
            for d in ["M", "T", "W", "T", "F", "S", "S"]:
                new = self.props.weekday_components.add()
                new.name = d
        if len(self.props.month_components) == 0:
            for m in ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]:
                new = self.props.month_components.add()
                new.name = m

    def load_recurrence_pattern_data(self, work_time):
        self.props.position = 0
        self.props.interval = 0
        self.props.occurrences = 0
        self.props.start_time = ""
        self.props.end_time = ""
        for component in self.props.day_components:
            component.is_specified = False
        for component in self.props.weekday_components:
            component.is_specified = False
        for component in self.props.month_components:
            component.is_specified = False
        if not work_time["RecurrencePattern"]:
            return
        recurrence_pattern = Data.recurrence_patterns[work_time["RecurrencePattern"]]
        for attribute in ["Position", "Interval", "Occurrences"]:
            if recurrence_pattern[attribute]:
                setattr(self.props, attribute.lower(), recurrence_pattern[attribute])
        for component in recurrence_pattern["DayComponent"] or []:
            self.props.day_components[component - 1].is_specified = True
        for component in recurrence_pattern["WeekdayComponent"] or []:
            self.props.weekday_components[component - 1].is_specified = True
        for component in recurrence_pattern["MonthComponent"] or []:
            self.props.month_components[component - 1].is_specified = True


class DisableEditingWorkTime(bpy.types.Operator):
    bl_idname = "bim.disable_editing_work_time"
    bl_label = "Disable Editing Work Time"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkCalendarProperties.active_work_time_id = 0
        return {"FINISHED"}


class EditWorkTime(bpy.types.Operator):
    bl_idname = "bim.edit_work_time"
    bl_label = "Edit Work Time"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.props = context.scene.BIMWorkCalendarProperties
        attributes = blenderbim.bim.helper.export_attributes(self.props.work_time_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_work_time",
            self.file,
            **{"work_time": self.file.by_id(self.props.active_work_time_id), "attributes": attributes},
        )

        work_time = Data.work_times[self.props.active_work_time_id]
        if work_time["RecurrencePattern"]:
            self.edit_recurrence_pattern(work_time["RecurrencePattern"])

        Data.load(IfcStore.get_file())
        bpy.ops.bim.disable_editing_work_time()
        return {"FINISHED"}

    def edit_recurrence_pattern(self, recurrence_pattern_id):
        recurrence_pattern = self.file.by_id(recurrence_pattern_id)
        attributes = {
            "Interval": self.props.interval if self.props.interval > 0 else None,
            "Occurrences": self.props.occurrences if self.props.occurrences > 0 else None,
        }
        applicable_data = {
            "DAILY": ["Interval", "Occurrences"],
            "WEEKLY": ["WeekdayComponent", "Interval", "Occurrences"],
            "MONTHLY_BY_DAY_OF_MONTH": ["DayComponent", "Interval", "Occurrences"],
            "MONTHLY_BY_POSITION": ["WeekdayComponent", "Position", "Interval", "Occurrences"],
            "BY_DAY_COUNT": ["Interval", "Occurrences"],
            "BY_WEEKDAY_COUNT": ["WeekdayComponent", "Interval", "Occurrences"],
            "YEARLY_BY_DAY_OF_MONTH": ["DayComponent", "MonthComponent", "Interval", "Occurrences"],
            "YEARLY_BY_POSITION": ["WeekdayComponent", "MonthComponent", "Position", "Interval", "Occurrences"],
        }
        if "Position" in applicable_data[recurrence_pattern.RecurrenceType]:
            attributes["Position"] = self.props.position if self.props.position != 0 else None
        if "DayComponent" in applicable_data[recurrence_pattern.RecurrenceType]:
            attributes["DayComponent"] = [i + 1 for i, c in enumerate(self.props.day_components) if c.is_specified]
        if "WeekdayComponent" in applicable_data[recurrence_pattern.RecurrenceType]:
            attributes["WeekdayComponent"] = [
                i + 1 for i, c in enumerate(self.props.weekday_components) if c.is_specified
            ]
        if "MonthComponent" in applicable_data[recurrence_pattern.RecurrenceType]:
            attributes["MonthComponent"] = [i + 1 for i, c in enumerate(self.props.month_components) if c.is_specified]
        ifcopenshell.api.run(
            "sequence.edit_recurrence_pattern",
            self.file,
            **{"recurrence_pattern": recurrence_pattern, "attributes": attributes},
        )


class RemoveWorkTime(bpy.types.Operator):
    bl_idname = "bim.remove_work_time"
    bl_label = "Remove Work Plan"
    bl_options = {"REGISTER", "UNDO"}
    work_time: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run("sequence.remove_work_time", self.file, **{"work_time": self.file.by_id(self.work_time)})
        Data.load(self.file)
        return {"FINISHED"}


class AssignRecurrencePattern(bpy.types.Operator):
    bl_idname = "bim.assign_recurrence_pattern"
    bl_label = "Assign Recurrence Pattern"
    bl_options = {"REGISTER", "UNDO"}
    work_time: bpy.props.IntProperty()
    recurrence_type: bpy.props.StringProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.assign_recurrence_pattern",
            self.file,
            **{"parent": self.file.by_id(self.work_time), "recurrence_type": self.recurrence_type},
        )
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class UnassignRecurrencePattern(bpy.types.Operator):
    bl_idname = "bim.unassign_recurrence_pattern"
    bl_label = "Unassign Recurrence Pattern"
    bl_options = {"REGISTER", "UNDO"}
    recurrence_pattern: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.unassign_recurrence_pattern",
            self.file,
            **{"recurrence_pattern": self.file.by_id(self.recurrence_pattern)},
        )
        Data.load(self.file)
        return {"FINISHED"}


class AddTimePeriod(bpy.types.Operator):
    bl_idname = "bim.add_time_period"
    bl_label = "Add Time Period"
    bl_options = {"REGISTER", "UNDO"}
    recurrence_pattern: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.props = context.scene.BIMWorkCalendarProperties
        self.file = IfcStore.get_file()
        try:
            start_time = parser.parse(self.props.start_time)
            end_time = parser.parse(self.props.end_time)
        except:
            return {"FINISHED"}
        ifcopenshell.api.run(
            "sequence.add_time_period",
            self.file,
            **{
                "recurrence_pattern": self.file.by_id(self.recurrence_pattern),
                "start_time": start_time,
                "end_time": end_time,
            },
        )
        self.props.start_time = ""
        self.props.end_time = ""
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class RemoveTimePeriod(bpy.types.Operator):
    bl_idname = "bim.remove_time_period"
    bl_label = "Remove Time Period"
    bl_options = {"REGISTER", "UNDO"}
    time_period: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.remove_time_period",
            self.file,
            **{"time_period": self.file.by_id(self.time_period)},
        )
        Data.load(self.file)
        return {"FINISHED"}


class EnableEditingTaskCalendar(bpy.types.Operator):
    bl_idname = "bim.enable_editing_task_calendar"
    bl_label = "Enable Editing Task Calendar"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        props.active_task_id = self.task
        props.editing_task_type = "CALENDAR"
        return {"FINISHED"}


class EditTaskCalendar(bpy.types.Operator):
    bl_idname = "bim.edit_task_calendar"
    bl_label = "Edit Task Calendar"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        task = self.file.by_id(self.task)
        ifcopenshell.api.run(
            "control.assign_control",
            self.file,
            **{
                "relating_control": self.file.by_id(self.work_calendar),
                "related_object": task,
            },
        )
        ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=task)
        Data.load(IfcStore.get_file())
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class RemoveTaskCalendar(bpy.types.Operator):
    bl_idname = "bim.remove_task_calendar"
    bl_label = "Remove Task Calendar"
    bl_options = {"REGISTER", "UNDO"}
    work_calendar: bpy.props.IntProperty()
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        task = self.file.by_id(self.task)
        ifcopenshell.api.run(
            "control.unassign_control",
            self.file,
            **{
                "relating_control": self.file.by_id(self.work_calendar),
                "related_object": task,
            },
        )
        ifcopenshell.api.run("sequence.cascade_schedule", self.file, task=task)
        Data.load(IfcStore.get_file())
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class EnableEditingTaskSequence(bpy.types.Operator):
    bl_idname = "bim.enable_editing_task_sequence"
    bl_label = "Enable Editing Task Sequence"
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        props.active_task_id = self.task
        props.editing_task_type = "SEQUENCE"
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class DisableEditingTaskTime(bpy.types.Operator):
    bl_idname = "bim.disable_editing_task_time"
    bl_label = "Disable Editing Task Time"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkScheduleProperties.active_task_time_id = 0
        bpy.ops.bim.disable_editing_task()
        return {"FINISHED"}


class EnableEditingSequenceAttributes(bpy.types.Operator):
    bl_idname = "bim.enable_editing_sequence_attributes"
    bl_label = "Enable Editing Sequence Attributes"
    bl_options = {"REGISTER", "UNDO"}
    sequence: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.props.active_sequence_id = self.sequence
        self.props.editing_sequence_type = "ATTRIBUTES"
        self.props.sequence_attributes.clear()
        self.enable_editing_sequence_attributes()
        return {"FINISHED"}

    def enable_editing_sequence_attributes(self):
        data = Data.sequences[self.sequence]
        blenderbim.bim.helper.import_attributes("IfcRelSequence", self.props.sequence_attributes, data)


class EnableEditingSequenceTimeLag(bpy.types.Operator):
    bl_idname = "bim.enable_editing_sequence_time_lag"
    bl_label = "Enable Editing Sequence Time Lag"
    bl_options = {"REGISTER", "UNDO"}
    sequence: bpy.props.IntProperty()
    lag_time: bpy.props.IntProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.props.active_sequence_id = self.sequence
        self.props.editing_sequence_type = "TIME_LAG"
        self.props.time_lag_attributes.clear()
        self.enable_editing_attributes()
        return {"FINISHED"}

    def enable_editing_attributes(self):
        data = Data.lag_times[self.lag_time]
        blenderbim.bim.helper.import_attributes(
            "IfcLagTime", self.props.time_lag_attributes, data, self.import_attributes
        )

    def import_attributes(self, name, prop, data):
        if name == "LagValue":
            if isinstance(data[name], isodate.Duration):
                prop.data_type = "string"
                prop.string_value = (
                    "" if prop.is_null else ifcopenshell.util.date.datetime2ifc(data[name], "IfcDuration")
                )
                return True
            else:
                prop.data_type = "float"
                prop.float_value = 0.0 if prop.is_null else data[name]
                return True


class UnassignLagTime(bpy.types.Operator):
    bl_idname = "bim.unassign_lag_time"
    bl_label = "Unassign Time Lag"
    bl_options = {"REGISTER", "UNDO"}
    sequence: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.unassign_lag_time", self.file, **{"rel_sequence": self.file.by_id(self.sequence)}
        )
        Data.load(IfcStore.get_file())
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class AssignLagTime(bpy.types.Operator):
    bl_idname = "bim.assign_lag_time"
    bl_label = "Assign Time Lag"
    bl_options = {"REGISTER", "UNDO"}
    sequence: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.assign_lag_time",
            self.file,
            **{"rel_sequence": self.file.by_id(self.sequence), "lag_value": "P0D"},
        )
        Data.load(IfcStore.get_file())
        return {"FINISHED"}


class EditSequenceAttributes(bpy.types.Operator):
    bl_idname = "bim.edit_sequence_attributes"
    bl_label = "Edit Sequence"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        attributes = blenderbim.bim.helper.export_attributes(props.sequence_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_sequence",
            self.file,
            **{"rel_sequence": self.file.by_id(props.active_sequence_id), "attributes": attributes},
        )
        Data.load(self.file)
        bpy.ops.bim.disable_editing_sequence()
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class EditSequenceTimeLag(bpy.types.Operator):
    bl_idname = "bim.edit_sequence_time_lag"
    bl_label = "Edit Time Lag"
    bl_options = {"REGISTER", "UNDO"}
    lag_time: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        attributes = blenderbim.bim.helper.export_attributes(props.time_lag_attributes)
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.edit_lag_time",
            self.file,
            **{"lag_time": self.file.by_id(self.lag_time), "attributes": attributes},
        )
        Data.load(self.file)
        bpy.ops.bim.disable_editing_sequence()
        bpy.ops.bim.load_task_properties()
        return {"FINISHED"}


class DisableEditingSequence(bpy.types.Operator):
    bl_idname = "bim.disable_editing_sequence"
    bl_label = "Disable Editing Sequence Attributes"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        context.scene.BIMWorkScheduleProperties.active_sequence_id = 0
        return {"FINISHED"}


class SelectTaskRelatedProducts(bpy.types.Operator):
    bl_idname = "bim.select_task_related_products"
    bl_label = "Select Similar Type"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        related_products = ifcopenshell.api.run(
            "sequence.get_related_products", self.file, **{"related_object": self.file.by_id(self.task)}
        )
        for obj in context.visible_objects:
            obj.select_set(False)
            if obj.BIMObjectProperties.ifc_definition_id in related_products:
                obj.select_set(True)
        return {"FINISHED"}


class VisualiseWorkScheduleDate(bpy.types.Operator):
    bl_idname = "bim.visualise_work_schedule_date"
    bl_label = "Visualise Work Schedule Date"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        self.date = parser.parse(props.visualisation_start, dayfirst=True, fuzzy=True)
        self.preprocess_tasks()
        for obj in bpy.data.objects:
            if not obj.BIMObjectProperties.ifc_definition_id:
                continue
            obj.color = (1.0, 1.0, 1.0, 1)
            obj.hide_set(False)
            if obj.BIMObjectProperties.ifc_definition_id in self.not_yet_started:
                obj.hide_set(True)
            elif obj.BIMObjectProperties.ifc_definition_id in self.started:
                obj.color = (0.0, 1.0, 0.0, 1)
            elif obj.BIMObjectProperties.ifc_definition_id in self.completed:
                pass
            else:
                obj.color = (1.0, 0.0, 0.0, 1)
        area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
        area.spaces[0].shading.color_type = "OBJECT"
        return {"FINISHED"}

    def preprocess_tasks(self):
        self.not_yet_started = set()
        self.started = set()
        self.completed = set()
        for rel in self.file.by_id(self.work_schedule).Controls or []:
            for related_object in rel.RelatedObjects:
                if related_object.is_a("IfcTask"):
                    self.preprocess_task(related_object)

    def preprocess_task(self, task):
        for rel in task.IsNestedBy or []:
            for related_object in rel.RelatedObjects:
                self.preprocess_task(related_object)
        start = helper.derive_date(task.id(), "ScheduleStart", is_earliest=True)
        finish = helper.derive_date(task.id(), "ScheduleFinish", is_latest=True)
        if not start or not finish:
            return
        products = [r.RelatingProduct.id() for r in task.HasAssignments or [] if r.is_a("IfcRelAssignsToProduct")]
        if not products:
            return
        if self.date < start:
            self.not_yet_started.update(products)
        elif self.date < finish:
            self.started.update(products)
        else:
            self.completed.update(products)


class VisualiseWorkScheduleDateRange(bpy.types.Operator):
    bl_idname = "bim.visualise_work_schedule_date_range"
    bl_label = "Visualise Work Schedule Date Range"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.props = context.scene.BIMWorkScheduleProperties
        self.start = parser.parse(self.props.visualisation_start, dayfirst=True, fuzzy=True)
        self.finish = parser.parse(self.props.visualisation_finish, dayfirst=True, fuzzy=True)
        self.duration = self.finish - self.start
        self.start_frame = 1
        self.total_frames = self.calculate_total_frames(context)
        self.preprocess_tasks()
        for obj in bpy.data.objects:
            if not obj.BIMObjectProperties.ifc_definition_id:
                continue
            product_frames = self.product_frames.get(obj.BIMObjectProperties.ifc_definition_id, [])
            for product_frame in product_frames:
                if product_frame["relationship"] == "input":
                    self.animate_input(obj, product_frame)
                elif product_frame["relationship"] == "output":
                    self.animate_output(obj, product_frame)

        area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
        area.spaces[0].shading.color_type = "OBJECT"
        context.scene.frame_start = self.start_frame
        context.scene.frame_end = self.start_frame + self.total_frames
        # with open("/home/dion/animation.json", "w") as json_file:
        #    guid_frames = {}
        #    for k, v in self.product_frames.items():
        #        guid_frames[self.file.by_id(k).GlobalId] = v
        #    json.dump(guid_frames, json_file)
        return {"FINISHED"}

    def animate_input(self, obj, product_frame):
        if product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]:
            self.animate_movement_from(obj, product_frame)
        else:
            self.animate_consumption(obj, product_frame)

    def animate_output(self, obj, product_frame):
        if product_frame["type"] in ["CONSTRUCTION", "INSTALLATION"]:
            self.animate_creation(obj, product_frame)
        elif product_frame["type"] in ["DEMOLITION", "DISMANTLE", "DISPOSAL", "REMOVAL"]:
            self.animate_destruction(obj, product_frame)
        elif product_frame["type"] in ["ATTENDANCE", "MAINTENANCE", "OPERATION", "RENOVATION"]:
            self.animate_operation(obj, product_frame)
        elif product_frame["type"] in ["LOGISTIC", "MOVE", "DISPOSAL"]:
            self.animate_movement_to(obj, product_frame)
        else:
            self.animate_operation(obj, product_frame)

    def animate_creation(self, obj, product_frame):
        obj.hide_viewport = True
        obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame)
        obj.hide_viewport = False
        obj.color = (0.0, 1.0, 0.0, 1)
        obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"])
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])

    def animate_destruction(self, obj, product_frame):
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=self.start_frame)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1)
        obj.color = (1.0, 0.0, 0.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.hide_viewport = True
        obj.color = (0.0, 0.0, 0.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])
        obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"])

    def animate_operation(self, obj, product_frame):
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=self.start_frame)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1)
        obj.color = (0.0, 0.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])

    def animate_movement_to(self, obj, product_frame):
        obj.hide_viewport = True
        obj.keyframe_insert(data_path="hide_viewport", frame=self.start_frame)
        obj.hide_viewport = False
        obj.color = (1.0, 1.0, 0.0, 1)
        obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["STARTED"])
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])

    def animate_movement_from(self, obj, product_frame):
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=self.start_frame)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1)
        obj.color = (1.0, 0.5, 0.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.hide_viewport = True
        obj.color = (0.0, 0.0, 0.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])
        obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"])

    def animate_consumption(self, obj, product_frame):
        obj.color = (1.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=self.start_frame)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"] - 1)
        obj.color = (0.0, 1.0, 1.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["STARTED"])
        obj.hide_viewport = True
        obj.color = (0.0, 0.0, 0.0, 1)
        obj.keyframe_insert(data_path="color", frame=product_frame["COMPLETED"])
        obj.keyframe_insert(data_path="hide_viewport", frame=product_frame["COMPLETED"])

    def calculate_total_frames(self, context):
        if self.props.speed_types == "FRAME_SPEED":
            return self.calculate_using_frames(
                self.start,
                self.finish,
                self.props.speed_animation_frames,
                isodate.parse_duration(self.props.speed_real_duration),
            )
        elif self.props.speed_types == "DURATION_SPEED":
            return self.calculate_using_duration(
                self.start,
                self.finish,
                context.scene.render.fps,
                isodate.parse_duration(self.props.speed_animation_duration),
                isodate.parse_duration(self.props.speed_real_duration),
            )
        elif self.props.speed_types == "MULTIPLIER_SPEED":
            return self.calculate_using_multiplier(
                self.start,
                self.finish,
                context.scene.render.fps,
                self.props.speed_multiplier,
            )

    def calculate_using_multiplier(self, start, finish, fps, multiplier):
        animation_time = (finish - start) / multiplier
        return animation_time.total_seconds() * fps

    def calculate_using_duration(self, start, finish, fps, animation_duration, real_duration):
        return self.calculate_using_multiplier(start, finish, fps, real_duration / animation_duration)

    def calculate_using_frames(self, start, finish, animation_frames, real_duration):
        return ((finish - start) / real_duration) * animation_frames

    def preprocess_tasks(self):
        self.product_frames = {}
        for rel in self.file.by_id(self.work_schedule).Controls or []:
            for related_object in rel.RelatedObjects:
                if related_object.is_a("IfcTask"):
                    self.preprocess_task(related_object)

    def preprocess_task(self, task):
        for rel in task.IsNestedBy or []:
            for related_object in rel.RelatedObjects:
                self.preprocess_task(related_object)
        start = helper.derive_date(task.id(), "ScheduleStart", is_earliest=True)
        finish = helper.derive_date(task.id(), "ScheduleFinish", is_latest=True)
        if not start or not finish:
            return
        for output_id in Data.tasks[task.id()]["Outputs"]:
            self.add_product_frame(output_id, task, start, finish, "output")
        for input_id in Data.tasks[task.id()]["Inputs"]:
            self.add_product_frame(input_id, task, start, finish, "input")

    def add_product_frame(self, product_id, task, start, finish, relationship):
        self.product_frames.setdefault(product_id, []).append(
            {
                "type": task.PredefinedType,
                "relationship": relationship,
                "STARTED": round(self.start_frame + (((start - self.start) / self.duration) * self.total_frames)),
                "COMPLETED": round(self.start_frame + (((finish - self.start) / self.duration) * self.total_frames)),
            }
        )


class BlenderBIM_DatePicker(bpy.types.Operator):
    bl_label = "Date Picker"
    bl_idname = "bim.datepicker"
    bl_options = {"REGISTER", "UNDO"}
    display_date: bpy.props.StringProperty(name="Display Date")
    selected_date: bpy.props.StringProperty(name="Selected Date")
    target_prop: bpy.props.StringProperty(name="Target date prop to set")

    def execute(self, context):
        helper.set_scene_prop(self.target_prop, self.selected_date)
        return {"FINISHED"}

    def draw(self, context):
        self.selected_date = helper.get_scene_prop("DatePickerProperties.selected_date") or helper.canonicalise_time(
            datetime.now()
        )
        current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
        current_month = (current_date.year, current_date.month)
        lines = calendar.monthcalendar(*current_month)
        month_title, week_titles = calendar.month(*current_month).splitlines()[:2]

        layout = self.layout
        row = layout.row()
        row.prop(self, "selected_date")

        split = layout.split()
        col = split.row()
        op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="")
        op.action = "previous"
        col = split.row()
        col.label(text=month_title.strip())
        col = split.row()
        col.alignment = "RIGHT"
        op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
        op.action = "next"

        row = layout.row(align=True)
        for title in week_titles.split():
            col = row.column(align=True)
            col.alignment = "CENTER"
            col.label(text=title.strip())

        for line in lines:
            row = layout.row(align=True)
            for i in line:
                col = row.column(align=True)
                if i == 0:
                    col.label(text="  ")
                else:
                    op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
                    selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year)
                    selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True)
                    op.selected_date = helper.canonicalise_time(selected_date)

    def invoke(self, context, event):
        self.display_date = helper.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now())
        context.scene.DatePickerProperties.display_date = self.display_date
        context.scene.DatePickerProperties.selected_date = self.display_date
        return context.window_manager.invoke_props_dialog(self)


class BlenderBIM_DatePickerSetDate(bpy.types.Operator):
    bl_label = "set date"
    bl_idname = "bim.datepicker_setdate"
    bl_options = {"REGISTER", "UNDO"}
    selected_date: bpy.props.StringProperty()

    def invoke(self, context, event):
        context.scene.DatePickerProperties.selected_date = self.selected_date
        return {"FINISHED"}


class BlenderBIM_RedrawDatePicker(bpy.types.Operator):
    bl_label = "redraw datepicker window"
    bl_idname = "bim.redraw_datepicker"
    bl_options = {"REGISTER", "UNDO"}
    action: bpy.props.StringProperty()

    def invoke(self, context, event):
        current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)

        if self.action == "previous":
            date_to_set = current_date - relativedelta.relativedelta(months=1)
        elif self.action == "next":
            date_to_set = current_date + relativedelta.relativedelta(months=1)

        context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set)

        return {"FINISHED"}


class RecalculateSchedule(bpy.types.Operator):
    bl_idname = "bim.recalculate_schedule"
    bl_label = "Recalculate Schedule"
    bl_options = {"REGISTER", "UNDO"}
    work_schedule: bpy.props.IntProperty()

    def execute(self, context):
        return IfcStore.execute_ifc_operator(self, context)

    def _execute(self, context):
        self.file = IfcStore.get_file()
        ifcopenshell.api.run(
            "sequence.recalculate_schedule", self.file, work_schedule=self.file.by_id(self.work_schedule)
        )
        Data.load(self.file)
        return {"FINISHED"}


class AddTaskColumn(bpy.types.Operator):
    bl_idname = "bim.add_task_column"
    bl_label = "Add Task Column"
    bl_options = {"REGISTER", "UNDO"}
    column_type: bpy.props.StringProperty()
    name: bpy.props.StringProperty()
    data_type: bpy.props.StringProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        new = self.props.columns.add()
        new.name = f"{self.column_type}.{self.name}"
        new.data_type = self.data_type
        return {"FINISHED"}


class RemoveTaskColumn(bpy.types.Operator):
    bl_idname = "bim.remove_task_column"
    bl_label = "Remove Task Column"
    bl_options = {"REGISTER", "UNDO"}
    name: bpy.props.StringProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.props.columns.remove(self.props.columns.find(self.name))
        return {"FINISHED"}


class SetTaskSortColumn(bpy.types.Operator):
    bl_idname = "bim.set_task_sort_column"
    bl_label = "Set Task Sort Column"
    bl_options = {"REGISTER", "UNDO"}
    column: bpy.props.StringProperty()

    def execute(self, context):
        self.props = context.scene.BIMWorkScheduleProperties
        self.props.sort_column = self.column
        bpy.ops.bim.enable_editing_tasks(work_schedule=self.props.active_work_schedule_id)
        return {"FINISHED"}


class LoadTaskResources(bpy.types.Operator):
    bl_idname = "bim.load_task_resources"
    bl_label = "Load Task Resources"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.props = context.scene.BIMWorkScheduleProperties
        self.tprops = context.scene.BIMTaskTreeProperties
        ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id
        self.props.task_resources.clear()
        for resource_id in Data.tasks[ifc_definition_id]["Resources"]:
            resource = self.file.by_id(resource_id)
            new = self.props.task_resources.add()
            new.ifc_definition_id = resource_id
            new.name = resource.Name or "Unnamed"
        return {"FINISHED"}


class LoadTaskInputs(bpy.types.Operator):
    bl_idname = "bim.load_task_inputs"
    bl_label = "Load Task Inputs"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.props = context.scene.BIMWorkScheduleProperties
        self.tprops = context.scene.BIMTaskTreeProperties
        ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id
        self.props.task_inputs.clear()
        for input_id in Data.tasks[ifc_definition_id]["Inputs"]:
            product = self.file.by_id(input_id)
            new = self.props.task_inputs.add()
            new.ifc_definition_id = input_id
            new.name = product.Name or "Unnamed"
        return {"FINISHED"}


class LoadTaskOutputs(bpy.types.Operator):
    bl_idname = "bim.load_task_outputs"
    bl_label = "Load Task Outputs"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        self.file = IfcStore.get_file()
        self.props = context.scene.BIMWorkScheduleProperties
        self.tprops = context.scene.BIMTaskTreeProperties
        ifc_definition_id = self.tprops.tasks[self.props.active_task_index].ifc_definition_id
        self.props.task_outputs.clear()
        for output_id in Data.tasks[ifc_definition_id]["Outputs"]:
            product = self.file.by_id(output_id)
            new = self.props.task_outputs.add()
            new.ifc_definition_id = output_id
            new.name = product.Name or "Unnamed"
        return {"FINISHED"}


class CalculateTaskDuration(bpy.types.Operator):
    bl_idname = "bim.calculate_task_duration"
    bl_label = "Calculate Task Duration"
    bl_options = {"REGISTER", "UNDO"}
    task: bpy.props.IntProperty()

    def execute(self, context):
        props = context.scene.BIMWorkScheduleProperties
        self.file = IfcStore.get_file()
        ifcopenshell.api.run("sequence.calculate_task_duration", self.file, task=self.file.by_id(self.task))
        Data.load(self.file)
        bpy.ops.bim.enable_editing_tasks(work_schedule=props.active_work_schedule_id)
        return {"FINISHED"}
