import atexit
import datetime as dt
import os
import re
import shutil
import struct
import subprocess
from pathlib import Path

import aem
import appscript
import osax
import psutil
from appscript import its, k as kw, mactypes
from appscript.reference import CommandError

import xlwings

from . import base_classes, mac_dict, utils
from .constants import ColorIndex
from .utils import (
    VersionNumber,
    col_name,
    fullname_url_to_local_path,
    int_to_rgb,
    np_datetime_to_datetime,
    read_config_sheet,
)

try:
    import pandas as pd
except ImportError:
    pd = None
try:
    import numpy as np
except ImportError:
    np = None
try:
    from PIL import ImageGrab
except ImportError:
    PIL = None


# Time types
time_types = (dt.date, dt.datetime)
if np:
    time_types = time_types + (np.datetime64,)

cell_errors = (
    "#DIV/0!",
    "#N/A",
    "#NAME?",
    "#NULL!",
    "#NUM!",
    "#REF!",
    "#VALUE!",
)


def _clean_value_data_element(
    value, datetime_builder, empty_as, number_builder, err_to_str
):
    if value == "" or value == kw.missing_value:
        return empty_as
    if isinstance(value, dt.datetime) and datetime_builder is not dt.datetime:
        value = datetime_builder(
            month=value.month,
            day=value.day,
            year=value.year,
            hour=value.hour,
            minute=value.minute,
            second=value.second,
            microsecond=value.microsecond,
            tzinfo=None,
        )
    elif number_builder is not None and isinstance(value, float):
        value = number_builder(value)
    return value


class Engine:
    @property
    def apps(self):
        return Apps()

    @property
    def name(self):
        return "excel"

    @property
    def type(self):
        return "desktop"

    @staticmethod
    def prepare_xl_data_element(x, options):
        if x is None:
            return ""
        elif pd and pd.isna(x):
            return ""
        elif np and isinstance(x, (np.floating, float)) and np.isnan(x):
            return ""
        elif np and isinstance(x, np.datetime64):
            # handle numpy.datetime64
            return np_datetime_to_datetime(x).replace(tzinfo=None)
        elif np and isinstance(x, np.number):
            return float(x)
        elif pd and isinstance(x, pd.Timestamp):
            # This transformation seems to be only needed on Python 2.6 (?)
            return x.to_pydatetime().replace(tzinfo=None)
        elif pd and isinstance(x, type(pd.NaT)):
            return None
        elif isinstance(x, dt.datetime):
            # Make datetime timezone naive
            return x.replace(tzinfo=None)
        elif isinstance(x, bool):
            # Must be tested before int!
            return x
        elif isinstance(x, int):
            # appscript packs integers larger than SInt32 but smaller than SInt64 as
            # typeSInt64, and integers larger than SInt64 as typeIEEE64BitFloatingPoint.
            # Excel silently ignores typeSInt64. (GH 227)
            return float(x)
        return x

    @staticmethod
    def clean_value_data(data, datetime_builder, empty_as, number_builder, err_to_str):
        return [
            [
                _clean_value_data_element(
                    c, datetime_builder, empty_as, number_builder, err_to_str
                )
                for c in row
            ]
            for row in data
        ]


engine = Engine()


class Apps(base_classes.Apps):
    def _iter_excel_instances(self):
        asn = subprocess.check_output(
            ["lsappinfo", "visibleprocesslist", "-includehidden"]
        ).decode("utf-8")
        for asn in asn.split(" "):
            if "Microsoft_Excel" in asn:
                pid_info = subprocess.check_output(
                    ["lsappinfo", "info", "-only", "pid", asn]
                ).decode("utf-8")
                if pid_info != '"pid"=[ NULL ] \n':
                    yield int(pid_info.split("=")[1])

    def keys(self):
        return list(self._iter_excel_instances())

    def add(self, spec=None, add_book=None, visible=None):
        return App(spec=spec, add_book=add_book, visible=visible)

    def __iter__(self):
        for pid in self._iter_excel_instances():
            yield App(xl=pid)

    def __len__(self):
        return len(list(self._iter_excel_instances()))

    def __getitem__(self, pid):
        if pid not in self.keys():
            raise KeyError("Could not find an Excel instance with this PID.")
        return App(xl=pid)


class App(base_classes.App):
    def __init__(self, spec=None, add_book=None, xl=None, visible=True):
        if xl is None:
            self._xl = appscript.app(
                name=spec or "Microsoft Excel",
                newinstance=True,
                terms=mac_dict,
                hide=not visible,
            )
            if visible:
                self.activate()  # Makes it behave like on Windows
        elif isinstance(xl, int):
            self._xl = appscript.app(pid=xl, terms=mac_dict)
        else:
            self._xl = xl

    @property
    def xl(self):
        return self._xl

    @xl.setter
    def xl(self, value):
        self._xl = value

    @property
    def api(self):
        return self.xl

    @property
    def engine(self):
        return engine

    @property
    def path(self):
        return hfs_to_posix_path(self.xl.path.get())

    @property
    def pid(self):
        data = (
            self.xl.AS_appdata.target()
            .addressdesc.coerce(aem.kae.typeKernelProcessID)
            .data
        )
        (pid,) = struct.unpack("i", data)
        return pid

    @property
    def version(self):
        return self.xl.version.get()

    @property
    def selection(self):
        sheet = self.books.active.sheets.active
        try:
            # fails if e.g. chart is selected
            return Range(sheet, self.xl.selection.get_address())
        except CommandError:
            return None

    def activate(self, steal_focus=False):
        asn = subprocess.check_output(
            ["lsappinfo", "visibleprocesslist", "-includehidden"]
        ).decode("utf-8")
        frontmost_asn = asn.split(" ")[0]
        pid_info_frontmost = subprocess.check_output(
            ["lsappinfo", "info", "-only", "pid", frontmost_asn]
        ).decode("utf-8")
        pid_frontmost = int(pid_info_frontmost.split("=")[1])
        try:
            appscript.app("System Events").processes[
                its.unix_id == self.pid
            ].frontmost.set(True)
            if not steal_focus:
                appscript.app("System Events").processes[
                    its.unix_id == pid_frontmost
                ].frontmost.set(True)
        except CommandError:
            pass  # may require root privileges (GH 1966)

    @property
    def visible(self):
        try:
            return (
                appscript.app("System Events")
                .processes[its.unix_id == self.pid]
                .visible.get()[0]
            )
        except CommandError:
            return None  # may require root privileges (GH 1966)

    @visible.setter
    def visible(self, visible):
        try:
            appscript.app("System Events").processes[
                its.unix_id == self.pid
            ].visible.set(visible)
        except CommandError:
            pass  # may require root privileges (GH 1966)

    def quit(self):
        self.xl.quit(saving=kw.no)

    def kill(self):
        psutil.Process(self.pid).kill()

    @property
    def screen_updating(self):
        return self.xl.screen_updating.get()

    @screen_updating.setter
    def screen_updating(self, value):
        self.xl.screen_updating.set(value)

    @property
    def display_alerts(self):
        return self.xl.display_alerts.get()

    @display_alerts.setter
    def display_alerts(self, value):
        self.xl.display_alerts.set(value)

    @property
    def enable_events(self):
        return self.xl.enable_events.get()

    @enable_events.setter
    def enable_events(self, value):
        self.xl.enable_events.set(value)

    @property
    def interactive(self):
        # TODO: replace with specific error when Exceptions are refactored
        raise xlwings.XlwingsError(
            "Getting or setting 'app.interactive' isn't supported on macOS."
        )

    @interactive.setter
    def interactive(self, value):
        # TODO: replace with specific error when Exceptions are refactored
        raise xlwings.XlwingsError(
            "Getting or setting 'app.interactive' isn't supported on macOS."
        )

    @property
    def startup_path(self):
        return hfs_to_posix_path(self.xl.startup_path.get())

    @property
    def calculation(self):
        return calculation_k2s[self.xl.calculation.get()]

    @calculation.setter
    def calculation(self, value):
        self.xl.calculation.set(calculation_s2k[value])

    def calculate(self):
        self.xl.calculate()

    @property
    def books(self):
        return Books(self)

    @property
    def hwnd(self):
        return None

    def run(self, macro, args):
        kwargs = {"arg{0}".format(i): n for i, n in enumerate(args, 1)}
        return self.xl.run_VB_macro(macro, **kwargs)

    @property
    def status_bar(self):
        return self.xl.status_bar.get()

    @status_bar.setter
    def status_bar(self, value):
        self.xl.status_bar.set(value)

    @property
    def cut_copy_mode(self):
        modes = {kw.cut_mode: "cut", kw.copy_mode: "copy"}
        return modes.get(self.xl.cut_copy_mode.get())

    @cut_copy_mode.setter
    def cut_copy_mode(self, value):
        self.xl.cut_copy_mode.set(value)

    def alert(self, prompt, title, buttons, mode, callback):
        # OSAX (Open Scripting Architecture Extension) instance for StandardAdditions
        # See /System/Library/ScriptingAdditions/StandardAdditions.osax
        sa = osax.OSAX(pid=self.pid)
        sa.activate()  # Activate app so dialog box will be visible
        modes = {
            None: kw.informational,
            "info": kw.informational,
            "critical": kw.critical,
        }
        buttons_dict = {
            None: "OK",
            "ok": "OK",
            "ok_cancel": ["Cancel", "OK"],
            "yes_no": ["No", "Yes"],
            "yes_no_cancel": ["Cancel", "No", "Yes"],
        }
        rv = sa.display_alert(
            "" if title is None else title,
            message="" if prompt is None else prompt,
            buttons=buttons_dict[buttons],
            as_=modes[mode],
        )
        return rv[kw.button_returned].lower()


class Books(base_classes.Books):
    def __init__(self, app):
        self.app = app

    @property
    def api(self):
        return None

    @property
    def active(self):
        return Book(self.app, self.app.xl.active_workbook.name.get())

    def __call__(self, name_or_index):
        b = Book(self.app, name_or_index)
        if not b.xl.exists():
            raise KeyError(name_or_index)
        return b

    def __contains__(self, key):
        return Book(self.app, key).xl.exists()

    def __len__(self):
        return self.app.xl.count(each=kw.workbook)

    def add(self):
        if self.app.visible:
            self.app.activate()
        xl = self.app.xl.make(new=kw.workbook)
        wb = Book(self.app, xl.name.get())
        return wb

    def open(
        self,
        fullname,
        update_links=None,
        read_only=None,
        format=None,
        password=None,
        write_res_password=None,
        ignore_read_only_recommended=None,
        origin=None,
        delimiter=None,
        editable=None,
        notify=None,
        converter=None,
        add_to_mru=None,
        local=None,
        corrupt_load=None,
    ):
        # TODO: format and origin currently require a native appscript keyword,
        #  read_only doesn't seem to work
        # Unsupported params
        if local is not None:
            # TODO: replace with specific error when Exceptions are refactored
            raise xlwings.XlwingsError("local is not supported on macOS")
        if corrupt_load is not None:
            # TODO: replace with specific error when Exceptions are refactored
            raise xlwings.XlwingsError("corrupt_load is not supported on macOS")
        # update_links: on Windows only constants 0 and 3 seem to be supported in
        # this context
        if update_links:
            update_links = kw.update_remote_and_external_links
        else:
            update_links = kw.do_not_update_links
        if self.app.visible:
            self.app.activate()
        filename = os.path.basename(fullname)
        self.app.xl.open_workbook(
            workbook_file_name=fullname,
            update_links=update_links,
            read_only=read_only,
            format=format,
            password=password,
            write_reserved_password=write_res_password,
            ignore_read_only_recommended=ignore_read_only_recommended,
            origin=origin,
            delimiter=delimiter,
            editable=editable,
            notify=notify,
            converter=converter,
            add_to_mru=add_to_mru,
            timeout=-1,
        )
        wb = Book(self.app, filename)
        return wb

    def __iter__(self):
        n = len(self)
        for i in range(n):
            yield Book(self.app, i + 1)


class Book(base_classes.Book):
    def __init__(self, app, name_or_index):
        self._app = app
        self.xl = app.xl.workbooks[name_or_index]

    @property
    def app(self):
        return self._app

    @property
    def api(self):
        return self.xl

    def json(self):
        raise NotImplementedError()

    @property
    def name(self):
        return self.xl.name.get()

    @property
    def sheets(self):
        return Sheets(self)

    def close(self):
        self.xl.close(saving=kw.no)

    def save(self, path, password):
        saved_path = self.xl.properties().get(kw.path)
        source_ext = os.path.splitext(self.name)[1] if saved_path else None
        target_ext = os.path.splitext(path)[1] if path else ".xlsx"
        if saved_path and source_ext == target_ext:
            file_format = self.xl.properties().get(kw.file_format)
        else:
            ext_to_file_format = {
                ".xlsx": kw.Excel_XML_file_format,
                ".xlsm": kw.macro_enabled_XML_file_format,
                ".xlsb": kw.Excel_binary_file_format,
                ".xltm": kw.macro_enabled_template_file_format,
                ".xltx": kw.template_file_format,
                ".xlam": kw.add_in_file_format,
                ".xls": kw.Excel98to2004_file_format,
                ".xlt": kw.Excel98to2004_template_file_format,
                ".xla": kw.Excel98to2004_add_in_file_format,
            }

            file_format = ext_to_file_format[target_ext]
        if (saved_path != "") and (path is None):
            # Previously saved: Save under existing name
            self.xl.save(timeout=-1)
        elif (
            (saved_path != "") and (path is not None) and (os.path.split(path)[0] == "")
        ):
            # Save existing book under new name in cwd if no path has been provided
            save_as_name = path
            path = os.path.join(os.getcwd(), path)
            hfs_path = posix_to_hfs_path(os.path.realpath(path))
            self.xl.save_workbook_as(
                filename=hfs_path,
                overwrite=True,
                file_format=file_format,
                timeout=-1,
                password=password,
            )
            self.xl = self.app.xl.workbooks[save_as_name]
        elif (saved_path == "") and (path is None):
            # Previously unsaved: Save under current name in current working directory
            save_as_name = self.xl.name.get() + ".xlsx"
            path = os.path.join(os.getcwd(), save_as_name)
            hfs_path = posix_to_hfs_path(os.path.realpath(path))
            self.xl.save_workbook_as(
                filename=hfs_path,
                overwrite=True,
                file_format=file_format,
                timeout=-1,
                password=password,
            )
            self.xl = self.app.xl.workbooks[save_as_name]
        elif path:
            # Save under new name/location
            hfs_path = posix_to_hfs_path(os.path.realpath(path))
            self.xl.save_workbook_as(
                filename=hfs_path,
                overwrite=True,
                file_format=file_format,
                timeout=-1,
                password=password,
            )
            self.xl = self.app.xl.workbooks[os.path.basename(path)]

    @property
    def fullname(self):
        display_alerts = self.app.display_alerts
        self.app.display_alerts = False
        # This causes a pop-up if there's a pw protected sheet, see #1377
        path = self.xl.properties().get(kw.full_name)
        if "://" in path:
            config = read_config_sheet(xlwings.Book(impl=self))
            self.app.display_alerts = display_alerts
            return fullname_url_to_local_path(
                url=path,
                sheet_onedrive_consumer_config=config.get("ONEDRIVE_CONSUMER_MAC"),
                sheet_onedrive_commercial_config=config.get("ONEDRIVE_COMMERCIAL_MAC"),
                sheet_sharepoint_config=config.get("SHAREPOINT_MAC"),
            )
        else:
            self.app.display_alerts = display_alerts
            return path

    @property
    def names(self):
        return Names(parent=self, xl=self.xl.named_items)

    def activate(self):
        self.xl.activate_object()

    def to_pdf(self, path, quality=None):
        # quality parameter for compatibility
        hfs_path = posix_to_hfs_path(path)
        display_alerts = self.app.display_alerts
        self.app.display_alerts = False
        if Path(path).exists():
            # Errors out with Parameter error (OSERROR: -50) otherwise
            os.unlink(path)
        self.xl.save(in_=hfs_path, as_=kw.PDF_file_format)
        self.app.display_alerts = display_alerts


class Sheets(base_classes.Sheets):
    def __init__(self, workbook):
        self.workbook = workbook

    @property
    def api(self):
        return None

    @property
    def active(self):
        return Sheet(self.workbook, self.workbook.xl.active_sheet.name.get())

    def __call__(self, name_or_index):
        return Sheet(self.workbook, name_or_index)

    def __len__(self):
        return self.workbook.xl.count(each=kw.worksheet)

    def __iter__(self):
        for i in range(len(self)):
            yield self(i + 1)

    def add(self, before=None, after=None, name=None):
        if before is None and after is None:
            before = self.workbook.app.books.active.sheets.active
        if before:
            position = before.xl.before
        else:
            position = after.xl.after
        xl = self.workbook.xl.make(new=kw.worksheet, at=position)
        if name is not None:
            xl.name.set(name)
            xl = self.workbook.xl.worksheets[name]
        return Sheet(self.workbook, xl.name.get())


class Sheet(base_classes.Sheet):
    def __init__(self, workbook, name_or_index):
        self.workbook = workbook
        self.xl = workbook.xl.worksheets[name_or_index]

    @property
    def api(self):
        return self.xl

    @property
    def name(self):
        return self.xl.name.get()

    @name.setter
    def name(self, value):
        self.xl.name.set(value)
        self.xl = self.workbook.xl.worksheets[value]

    @property
    def names(self):
        return Names(parent=self, xl=self.xl.named_items)

    @property
    def book(self):
        return self.workbook

    @property
    def index(self):
        return self.xl.entry_index.get()

    def range(self, arg1, arg2=None):
        if isinstance(arg1, tuple):
            if len(arg1) == 2:
                if 0 in arg1:
                    raise IndexError(
                        "Attempted to access 0-based Range. "
                        "xlwings/Excel Ranges are 1-based."
                    )
                row1 = arg1[0]
                col1 = arg1[1]
                address1 = self.xl.rows[row1].columns[col1].get_address()
            elif len(arg1) == 4:
                return Range(self, arg1)
            else:
                raise ValueError("Invalid parameters")
        elif isinstance(arg1, Range):
            row1 = min(arg1.row, arg2.row)
            col1 = min(arg1.column, arg2.column)
            address1 = self.xl.rows[row1].columns[col1].get_address()
        elif isinstance(arg1, str):
            address1 = arg1.split(":")[0]
        else:
            raise ValueError("Invalid parameters")

        if isinstance(arg2, tuple):
            if 0 in arg2:
                raise IndexError(
                    "Attempted to access 0-based Range. "
                    "xlwings/Excel Ranges are 1-based."
                )
            row2 = arg2[0]
            col2 = arg2[1]
            address2 = self.xl.rows[row2].columns[col2].get_address()
        elif isinstance(arg2, Range):
            row2 = max(arg1.row + arg1.shape[0] - 1, arg2.row + arg2.shape[0] - 1)
            col2 = max(arg1.column + arg1.shape[1] - 1, arg2.column + arg2.shape[1] - 1)
            address2 = self.xl.rows[row2].columns[col2].get_address()
        elif isinstance(arg2, str):
            address2 = arg2
        elif arg2 is None:
            if isinstance(arg1, str) and len(arg1.split(":")) == 2:
                address2 = arg1.split(":")[1]
            else:
                return Range(self, "{0}".format(address1))
        else:
            raise ValueError("Invalid parameters")

        return Range(self, "{0}:{1}".format(address1, address2))

    @property
    def cells(self):
        return self.range(
            (1, 1), (self.xl.count(each=kw.row), self.xl.count(each=kw.column))
        )

    def activate(self):
        self.xl.activate_object()

    def select(self):
        self.xl.select()

    def clear_contents(self):
        self.xl.used_range.clear_contents()

    def clear_formats(self):
        self.xl.used_range.clear_formats()

    def clear(self):
        self.xl.used_range.clear_range()

    def autofit(self, axis=None):
        num_columns = self.xl.count(each=kw.column)
        num_rows = self.xl.count(each=kw.row)
        address = self.range((1, 1), (num_rows, num_columns)).address
        alerts_state = self.book.app.screen_updating
        self.book.app.screen_updating = False
        if axis == "rows" or axis == "r":
            self.xl.rows[address].autofit()
        elif axis == "columns" or axis == "c":
            self.xl.columns[address].autofit()
        elif axis is None:
            self.xl.rows[address].autofit()
            self.xl.columns[address].autofit()
        self.book.app.screen_updating = alerts_state

    def delete(self):
        alerts_state = self.book.app.xl.display_alerts.get()
        self.book.app.xl.display_alerts.set(False)
        self.xl.delete()
        self.book.app.xl.display_alerts.set(alerts_state)

    def copy(self, before, after):
        if before:
            before = before.xl
        if after:
            after = after.xl
        self.xl.copy_worksheet(before_=before, after_=after)

    @property
    def charts(self):
        return Charts(self)

    @property
    def shapes(self):
        return Shapes(self)

    @property
    def tables(self):
        return Tables(self)

    @property
    def pictures(self):
        return Pictures(self)

    @property
    def used_range(self):
        return Range(self, self.xl.used_range.get_address())

    @property
    def visible(self):
        return True if self.xl.visible.get() == kw.sheet_visible else False

    @visible.setter
    def visible(self, value):
        self.xl.visible.set(value)

    @property
    def page_setup(self):
        return PageSetup(self, self.xl.page_setup_object)


class Range(base_classes.Range):
    def __init__(self, sheet, address):
        self.sheet = sheet
        self.options = None  # Assigned by main.Range to keep API of sheet.range clean
        if isinstance(address, tuple):
            self._coords = address
            row, col, nrows, ncols = address
            if nrows and ncols:
                self.xl = sheet.xl.cells[
                    "%s:%s"
                    % (
                        sheet.xl.rows[row].columns[col].get_address(),
                        sheet.xl.rows[row + nrows - 1]
                        .columns[col + ncols - 1]
                        .get_address(),
                    )
                ]
            else:
                self.xl = None
        else:
            self.xl = sheet.xl.cells[address]
            self._coords = None

    @property
    def coords(self):
        if self._coords is None:
            self._coords = (
                self.xl.first_row_index.get(),
                self.xl.first_column_index.get(),
                self.xl.count(each=kw.row),
                self.xl.count(each=kw.column),
            )
        return self._coords

    @property
    def api(self):
        return self.xl

    def __len__(self):
        return self.coords[2] * self.coords[3]

    @property
    def row(self):
        return self.coords[0]

    @property
    def column(self):
        return self.coords[1]

    @property
    def shape(self):
        return self.coords[2], self.coords[3]

    @property
    def raw_value(self):
        def ensure_2d(values):
            # Usually done in converter, but macOS doesn't deliver any info about
            # errors with values
            if not isinstance(values, list):
                return [[values]]
            elif not isinstance(values[0], list):
                return [values]

        if self.xl is not None:
            values = self.xl.value.get()
            if self.options.get("err_to_str", False):
                string_values = self.xl.string_value.get()
                values = ensure_2d(values)
                string_values = ensure_2d(string_values)
                for row_ix, row in enumerate(string_values):
                    for col_ix, c in enumerate(row):
                        if c in cell_errors:
                            values[row_ix][col_ix] = c
            return values

    @raw_value.setter
    def raw_value(self, value):
        if self.xl is not None:
            self.xl.value.set(value)

    def clear_contents(self):
        if self.xl is not None:
            alerts_state = self.sheet.book.app.screen_updating
            self.sheet.book.app.screen_updating = False
            self.xl.clear_contents()
            self.sheet.book.app.screen_updating = alerts_state

    def clear_formats(self):
        if self.xl is not None:
            alerts_state = self.sheet.book.app.screen_updating
            self.sheet.book.app.screen_updating = False
            self.xl.clear_formats()
            self.sheet.book.app.screen_updating = alerts_state

    def clear(self):
        if self.xl is not None:
            alerts_state = self.sheet.book.app.screen_updating
            self.sheet.book.app.screen_updating = False
            self.xl.clear_range()
            self.sheet.book.app.screen_updating = alerts_state

    def end(self, direction):
        direction = directions_s2k.get(direction, direction)
        return Range(self.sheet, self.xl.get_end(direction=direction).get_address())

    @property
    def formula(self):
        if self.xl is not None:
            return self.xl.formula.get()

    @formula.setter
    def formula(self, value):
        if self.xl is not None:
            self.xl.formula.set(value)

    @property
    def formula2(self):
        if self.xl is not None:
            return self.xl.formula2.get()

    @formula2.setter
    def formula2(self, value):
        if self.xl is not None:
            self.xl.formula2.set(value)

    @property
    def formula_array(self):
        if self.xl is not None:
            rv = self.xl.formula_array.get()
            return None if rv == kw.missing_value else rv

    @formula_array.setter
    def formula_array(self, value):
        if self.xl is not None:
            self.xl.formula_array.set(value)

    @property
    def font(self):
        return Font(self, self.xl.font_object)

    @property
    def column_width(self):
        if self.xl is not None:
            rv = self.xl.column_width.get()
            return None if rv == kw.missing_value else rv
        else:
            return 0

    @column_width.setter
    def column_width(self, value):
        if self.xl is not None:
            self.xl.column_width.set(value)

    @property
    def row_height(self):
        if self.xl is not None:
            rv = self.xl.row_height.get()
            return None if rv == kw.missing_value else rv
        else:
            return 0

    @row_height.setter
    def row_height(self, value):
        if self.xl is not None:
            self.xl.row_height.set(value)

    @property
    def width(self):
        if self.xl is not None:
            return self.xl.width.get()
        else:
            return 0

    @property
    def height(self):
        if self.xl is not None:
            return self.xl.height.get()
        else:
            return 0

    @property
    def left(self):
        return self.xl.properties().get(kw.left_position)

    @property
    def top(self):
        return self.xl.properties().get(kw.top)

    @property
    def has_array(self):
        if self.xl is not None:
            return self.xl.has_array.get()

    @property
    def number_format(self):
        if self.xl is not None:
            rv = self.xl.number_format.get()
            return None if rv == kw.missing_value else rv

    @number_format.setter
    def number_format(self, value):
        if self.xl is not None:
            alerts_state = self.sheet.book.app.screen_updating
            self.sheet.book.app.screen_updating = False
            self.xl.number_format.set(value)
            self.sheet.book.app.screen_updating = alerts_state

    def get_address(self, row_absolute, col_absolute, external):
        if self.xl is not None:
            return self.xl.get_address(
                row_absolute=row_absolute,
                column_absolute=col_absolute,
                external=external,
            )

    @property
    def address(self):
        if self.xl is not None:
            return self.xl.get_address()
        else:
            row, col, nrows, ncols = self.coords
            return "$%s$%s{%sx%s}" % (col_name(col), row, nrows, ncols)

    @property
    def current_region(self):
        return Range(self.sheet, self.xl.current_region.get_address())

    def autofit(self, axis=None):
        if self.xl is not None:
            address = self.address
            alerts_state = self.sheet.book.app.screen_updating
            self.sheet.book.app.screen_updating = False
            if axis == "rows" or axis == "r":
                self.sheet.xl.rows[address].autofit()
            elif axis == "columns" or axis == "c":
                self.sheet.xl.columns[address].autofit()
            elif axis is None:
                self.sheet.xl.rows[address].autofit()
                self.sheet.xl.columns[address].autofit()
            self.sheet.book.app.screen_updating = alerts_state

    def insert(self, shift=None, copy_origin=None):
        # copy_origin is not supported on mac
        shifts = {"down": kw.shift_down, "right": kw.shift_to_right, None: None}
        self.xl.insert_into_range(shift=shifts[shift])

    def delete(self, shift=None):
        shifts = {"up": kw.shift_up, "left": kw.shift_to_left, None: None}
        self.xl.delete_range(shift=shifts[shift])

    def copy(self, destination=None):
        self.xl.copy_range(destination=destination.api if destination else None)

    def paste(self, paste=None, operation=None, skip_blanks=False, transpose=False):
        pastes = {
            # all_merging_conditional_formats unsupported on mac
            "all": kw.paste_all,
            "all_except_borders": kw.paste_all_except_borders,
            "all_using_source_theme": kw.paste_all_using_source_theme,
            "column_widths": kw.paste_column_widths,
            "comments": kw.paste_comments,
            "formats": kw.paste_formats,
            "formulas": kw.paste_formulas,
            "formulas_and_number_formats": kw.paste_formulas_and_number_formats,
            "validation": kw.paste_validation,
            "values": kw.paste_values,
            "values_and_number_formats": kw.paste_values_and_number_formats,
            None: None,
        }

        operations = {
            "add": kw.paste_special_operation_add,
            "divide": kw.paste_special_operation_divide,
            "multiply": kw.paste_special_operation_multiply,
            "subtract": kw.paste_special_operation_subtract,
            None: None,
        }

        self.xl.paste_special(
            what=pastes[paste],
            operation=operations[operation],
            skip_blanks=skip_blanks,
            transpose=transpose,
        )

    @property
    def hyperlink(self):
        try:
            return self.xl.hyperlinks[1].address.get()
        except CommandError:
            raise Exception("The cell doesn't seem to contain a hyperlink!")

    def add_hyperlink(self, address, text_to_display=None, screen_tip=None):
        if self.xl is not None:
            self.xl.make(
                at=self.xl,
                new=kw.hyperlink,
                with_properties={
                    kw.address: address,
                    kw.text_to_display: text_to_display,
                    kw.screen_tip: screen_tip,
                },
            )

    @property
    def color(self):
        if (
            not self.xl
            or self.xl.interior_object.color_index.get() == kw.color_index_none
        ):
            return None
        else:
            return tuple(self.xl.interior_object.color.get())

    @color.setter
    def color(self, color_or_rgb):
        if isinstance(color_or_rgb, str):
            color_or_rgb = utils.hex_to_rgb(color_or_rgb)
        if self.xl is not None:
            if color_or_rgb is None:
                self.xl.interior_object.color_index.set(ColorIndex.xlColorIndexNone)
            elif isinstance(color_or_rgb, int):
                self.xl.interior_object.color.set(int_to_rgb(color_or_rgb))
            else:
                self.xl.interior_object.color.set(color_or_rgb)

    @property
    def name(self):
        if not self.xl:
            return None
        xl = self.xl.named_item
        if xl.get() == kw.missing_value:
            return None
        else:
            return Name(self.sheet.book, xl=xl)

    @name.setter
    def name(self, value):
        if self.xl is not None:
            self.xl.name.set(value)

    def __call__(self, arg1, arg2=None):
        if arg2 is None:
            col = (arg1 - 1) % self.shape[1]
            row = int((arg1 - 1 - col) / self.shape[1])
            return self(1 + row, 1 + col)
        else:
            return Range(
                self.sheet,
                self.sheet.xl.rows[self.row + arg1 - 1]
                .columns[self.column + arg2 - 1]
                .get_address(),
            )

    @property
    def rows(self):
        row = self.row
        col1 = self.column
        col2 = col1 + self.shape[1] - 1
        return [
            self.sheet.range((row + i, col1), (row + i, col2))
            for i in range(self.shape[0])
        ]

    @property
    def columns(self):
        col = self.column
        row1 = self.row
        row2 = row1 + self.shape[0] - 1
        sht = self.sheet
        return [
            sht.range((row1, col + i), (row2, col + i)) for i in range(self.shape[1])
        ]

    def select(self):
        if self.xl is not None:
            return self.xl.select()

    @property
    def merge_area(self):
        return Range(self.sheet, self.xl.merge_area.get_address())

    @property
    def merge_cells(self):
        return self.xl.merge_cells.get()

    def merge(self, across):
        self.xl.merge(across=across)

    def unmerge(self):
        self.xl.unmerge()

    @property
    def table(self):
        if self.xl.list_object.name.get() == kw.missing_value:
            return None
        else:
            return Table(self.sheet, self.xl.list_object.name.get())

    @property
    def characters(self):
        # This is broken with AppleScript/Excel 2016
        return Characters(parent=self, xl=self.xl.characters)

    @property
    def wrap_text(self):
        return self.xl.wrap_text.get()

    @wrap_text.setter
    def wrap_text(self, value):
        self.xl.wrap_text.set(value)

    @property
    def note(self):
        try:
            # No easy way to check whether there's a comment like on Windows
            return (
                Note(parent=self, xl=self.xl.Excel_comment)
                if self.xl.Excel_comment.Excel_comment_text()
                else None
            )
        except appscript.reference.CommandError:
            return None

    def copy_picture(self, appearance, format):
        _appearance = {"screen": kw.screen, "printer": kw.printer}
        _format = {"picture": kw.picture, "bitmap": kw.bitmap}
        self.xl.copy_picture(appearance=_appearance[appearance], format=_format[format])

    def to_png(self, path):
        self.copy_picture(appearance="screen", format="bitmap")
        im = ImageGrab.grabclipboard()
        im.save(path)

    def to_pdf(self, path, quality=None):
        raise xlwings.XlwingsError("Range.to_pdf() isn't supported on macOS.")

    def autofill(self, destination, type_):
        types = {
            "fill_copy": kw.fill_copy,
            "fill_days": kw.fill_days,
            "fill_default": kw.fill_default,
            "fill_formats": kw.fill_formats,
            "fill_months": kw.fill_months,
            "fill_series": kw.fill_series,
            "fill_values": kw.fill_values,
            "fill_weekdays": kw.fill_weekdays,
            "fill_years": kw.fill_years,
            "growth_trend": kw.growth_trend,
            "linear_trend": kw.linear_trend,
            "flash_fill": kw.flashfill,
        }
        self.xl.autofill(destination=destination.api, type=types[type_])


class Shape(base_classes.Shape):
    def __init__(self, parent, key):
        self._parent = parent
        self.xl = parent.xl.shapes[key]

    @property
    def parent(self):
        return self._parent

    @property
    def api(self):
        return self.xl

    @property
    def name(self):
        return self.xl.name.get()

    @name.setter
    def name(self, value):
        self.xl.name.set(value)

    @property
    def type(self):
        return shape_types_k2s[self.xl.shape_type.get()]

    @property
    def left(self):
        return self.xl.left_position.get()

    @left.setter
    def left(self, value):
        self.xl.left_position.set(value)

    @property
    def top(self):
        return self.xl.top.get()

    @top.setter
    def top(self, value):
        self.xl.top.set(value)

    @property
    def width(self):
        return self.xl.width.get()

    @width.setter
    def width(self, value):
        self.xl.width.set(value)

    @property
    def height(self):
        return self.xl.height.get()

    @height.setter
    def height(self, value):
        self.xl.height.set(value)

    def delete(self):
        self.xl.delete()

    @property
    def index(self):
        return self.xl.entry_index.get()

    def activate(self):
        # self.xl.activate_object()  # doesn't work?
        self.xl.select()

    def scale_height(self, factor, relative_to_original_size, scale):
        self.xl.scale_height(
            scale=scaling[scale],
            relative_to_original_size=relative_to_original_size,
            factor=factor,
        )

    def scale_width(self, factor, relative_to_original_size, scale):
        self.xl.scale_width(
            scale=scaling[scale],
            relative_to_original_size=relative_to_original_size,
            factor=factor,
        )

    @property
    def text(self):
        if self.xl.shape_text_frame.has_text.get():
            return self.xl.shape_text_frame.text_range.content.get()

    @text.setter
    def text(self, value):
        self.xl.shape_text_frame.text_range.content.set(value)

    @property
    def font(self):
        return Font(self, self.xl.shape_text_frame.text_range.font)

    @property
    def characters(self):
        raise AttributeError("Characters isn't supported on macOS with shapes.")


class Font(base_classes.Font):
    def __init__(self, parent, xl):
        # xl can be font or font_object
        self.parent = parent
        self.xl = xl

    @property
    def api(self):
        return self.xl

    @property
    def bold(self):
        return self.xl.bold.get()

    @bold.setter
    def bold(self, value):
        self.xl.bold.set(value)

    @property
    def italic(self):
        return self.xl.italic.get()

    @italic.setter
    def italic(self, value):
        self.xl.italic.set(value)

    @property
    def size(self):
        return self.xl.font_size.get()

    @size.setter
    def size(self, value):
        self.xl.font_size.set(value)

    @property
    def color(self):
        if isinstance(self.parent, Range):
            return tuple(self.xl.color.get())
        elif isinstance(self.parent, Shape):
            return tuple(self.xl.font_color.get())

    @color.setter
    def color(self, color_or_rgb):
        if self.xl is not None:
            if isinstance(self.parent, (Range, Characters)):
                obj = self.xl.color
            elif isinstance(self.parent, Shape):
                obj = self.xl.font_color

            if isinstance(color_or_rgb, int):
                obj.set(int_to_rgb(color_or_rgb))
            else:
                obj.set(color_or_rgb)

    @property
    def name(self):
        if isinstance(self.parent, Range):
            return self.xl.name.get()
        elif isinstance(self.parent, Shape):
            return self.xl.font_name.get()

    @name.setter
    def name(self, value):
        if isinstance(self.parent, Range):
            self.xl.name.set(value)
        elif isinstance(self.parent, Shape):
            self.xl.font_name.set(value)


class Characters(base_classes.Characters):
    def __init__(self, parent, xl):
        self.parent = parent
        self.xl = xl

    @property
    def api(self):
        return self.xl

    @property
    def text(self):
        return self.xl.content.get()

    @property
    def font(self):
        return Font(self, self.xl.font_object)

    def __getitem__(self, item):
        # TODO: This is broken with AppleScript and Excel 2016:
        # set bold of font object of (characters 5 thru 9 of range "A1") to true
        # https://answers.microsoft.com/en-us/msoffice/forum/
        #  msoffice_excel-mso_mac-msoversion_other/applescript-and-excel-problem/
        #  6e5a50b1-6209-4fbf-91f4-6d6674f1e488
        if isinstance(item, slice):
            return Characters(
                parent=self.parent,
                xl=self.xl[
                    item.start + 1
                    if item.start
                    else None : item.stop
                    if item.stop
                    else len(self.text)
                ],
            )
        else:
            return Characters(parent=self.parent, xl=self.xl[item + 1 : item + 1])


class PageSetup(base_classes.PageSetup):
    def __init__(self, parent, xl):
        self.parent = parent
        self.xl = xl

    @property
    def api(self):
        return self.xl

    @property
    def print_area(self):
        value = self.xl.print_area.get()
        if value == kw.missing_value:
            return None
        else:
            return self.xl.print_area.get()

    @print_area.setter
    def print_area(self, value):
        self.xl.print_area.set("" if value is None else value)


class Note(base_classes.Note):
    def __init__(self, parent, xl):
        self.parent = parent
        self.xl = xl

    def api(self):
        return self.xl

    @property
    def text(self):
        return self.xl.Excel_comment_text()

    @text.setter
    def text(self, value):
        self.xl.Excel_comment_text(text=value)

    def delete(self):
        self.parent.xl.clear_Excel_comments()


class Collection(base_classes.Collection):
    def __init__(self, parent):
        self._parent = parent
        self.xl = getattr(self.parent.xl, self._attr)

    @property
    def parent(self):
        return self._parent

    @property
    def api(self):
        return self.xl

    def __call__(self, key):
        if not self.xl[key].exists():
            raise KeyError(key)
        return self._wrap(self.parent, key)

    def __len__(self):
        return self.parent.xl.count(each=self._kw)

    def __iter__(self):
        for i in range(len(self)):
            yield self(i + 1)

    def __contains__(self, key):
        return self.xl[key].exists()


class Table(base_classes.Table):
    def __init__(self, parent, key):
        self._parent = parent
        self.xl = parent.xl.list_objects[key]

    @property
    def parent(self):
        return self._parent

    @property
    def api(self):
        return self.xl

    @property
    def name(self):
        return self.xl.name.get()

    @name.setter
    def name(self, value):
        self.xl.name.set(value)
        self.xl = self.parent.xl.list_objects[value]

    @property
    def data_body_range(self):
        if self.xl.cell_table.get() == kw.missing_value:
            return
        else:
            return Range(self.parent, self.xl.cell_table.get_address())

    @property
    def display_name(self):
        return self.xl.display_name.get()

    @display_name.setter
    def display_name(self, value):
        # Changing the display_name also changes the name
        self.xl.display_name.set(value)
        self.xl = self.parent.xl.list_objects[value]

    @property
    def header_row_range(self):
        if self.xl.header_row.get() == kw.missing_value:
            return
        else:
            return Range(self.parent, self.xl.header_row.get_address())

    @property
    def insert_row_range(self):
        if self.xl.insert_row.get() == kw.missing_value:
            return
        else:
            return Range(self.parent, self.xl.insert_row.get_address())

    @property
    def range(self):
        return Range(self.parent, self.xl.range_object.get_address())

    @property
    def show_autofilter(self):
        return self.xl.show_autofilter.get()

    @show_autofilter.setter
    def show_autofilter(self, value):
        self.xl.show_autofilter.set(value)

    @property
    def show_headers(self):
        return self.xl.show_headers.get()

    @show_headers.setter
    def show_headers(self, value):
        self.xl.show_headers.set(value)

    @property
    def show_table_style_column_stripes(self):
        return self.xl.show_table_style_column_stripes.get()

    @show_table_style_column_stripes.setter
    def show_table_style_column_stripes(self, value):
        self.xl.show_table_style_column_stripes.set(value)

    @property
    def show_table_style_first_column(self):
        return self.xl.show_table_style_first_column.get()

    @show_table_style_first_column.setter
    def show_table_style_first_column(self, value):
        self.xl.show_table_style_first_column.set(value)

    @property
    def show_table_style_last_column(self):
        return self.xl.show_table_style_last_column.get()

    @show_table_style_last_column.setter
    def show_table_style_last_column(self, value):
        self.xl.show_table_style_last_column.set(value)

    @property
    def show_table_style_row_stripes(self):
        return self.xl.show_table_style_row_stripes.get()

    @show_table_style_row_stripes.setter
    def show_table_style_row_stripes(self, value):
        self.xl.show_table_style_row_stripes.set(value)

    @property
    def show_totals(self):
        return self.xl.total.get()

    @show_totals.setter
    def show_totals(self, value):
        self.xl.total.set(value)

    @property
    def table_style(self):
        return self.xl.table_style.properties().get(kw.name)

    @table_style.setter
    def table_style(self, value):
        self.xl.table_style.set(value)

    @property
    def totals_row_range(self):
        if self.xl.total_row.get() == kw.missing_value:
            return
        else:
            return Range(self.parent, self.xl.total_row.get_address())

    def resize(self, range):
        self.xl.resize(range=range.api)


class Tables(Collection, base_classes.Tables):
    _attr = "list_objects"
    _kw = kw.list_object
    _wrap = Table

    def add(
        self,
        source_type=None,
        source=None,
        link_source=None,
        has_headers=None,
        destination=None,
        table_style_name=None,
        name=None,
    ):
        header_row = {
            True: kw.header_yes,
            False: kw.header_no,
            "guess": kw.header_guess,
        }
        sheet_index = self.parent.xl.entry_index.get()
        table = Table(
            self.parent,
            self.parent.xl.make(
                at=self.parent.book.xl.sheets[sheet_index],
                new=kw.list_object,
                with_properties={
                    kw.source_type: kw.src_range,
                    kw.range_object: source.api,
                    kw.header_row: header_row[has_headers],
                    kw.table_style: table_style_name,
                },
            ).name.get(),
        )
        if name is not None:
            table.name = name
        return table


class Chart(base_classes.Chart):
    def __init__(self, parent, key):
        self._parent = parent
        if isinstance(parent, Sheet):
            self.xl_obj = parent.xl.chart_objects[key]
            self.xl = self.xl_obj.chart
        else:
            self.xl_obj = None
            self.xl = self.charts[key]

    @property
    def parent(self):
        return self._parent

    @property
    def api(self):
        return self.xl_obj, self.xl

    def set_source_data(self, rng):
        self.xl.set_source_data(source=rng.xl)

    @property
    def name(self):
        if self.xl_obj is not None:
            return self.xl_obj.name.get()
        else:
            return self.xl.name.get()

    @name.setter
    def name(self, value):
        if self.xl_obj is not None:
            self.xl_obj.name.set(value)
        else:
            self.xl.name.get(value)

    @property
    def chart_type(self):
        return chart_types_k2s[self.xl.chart_type.get()]

    @chart_type.setter
    def chart_type(self, value):
        self.xl.chart_type.set(chart_types_s2k[value])

    @property
    def left(self):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        return self.xl_obj.left_position.get()

    @left.setter
    def left(self, value):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        self.xl_obj.left_position.set(value)

    @property
    def top(self):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        return self.xl_obj.top.get()

    @top.setter
    def top(self, value):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        self.xl_obj.top.set(value)

    @property
    def width(self):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        return self.xl_obj.width.get()

    @width.setter
    def width(self, value):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        self.xl_obj.width.set(value)

    @property
    def height(self):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        return self.xl_obj.height.get()

    @height.setter
    def height(self, value):
        if self.xl_obj is None:
            raise Exception("This chart is not embedded.")
        self.xl_obj.height.set(value)

    def delete(self):
        self.xl_obj.delete()

    def to_png(self, path):
        raise xlwings.XlwingsError("Chart.to_png() isn't supported on macOS.")
        # Both versions should work, but seem to be broken with Excel 2016
        #
        # Version 1
        # import uuid
        # temp_path = posix_to_hfs_path(os.path.expanduser("~")
        #                               + f"/Library/Containers/com.microsoft.Excel/"
        #                                 f"Data/{uuid.uuid4()}.png")
        # self.xl.save_as(filename=temp_path)
        # shutil.copy2(temp_path, path)
        # try:
        #     os.unlink(temp_path)
        # except:
        #     pass
        #
        # Version 2
        # self.xl_obj.save_as_picture(file_name=posix_to_hfs_path('...'),
        #                             picture_type=kw.save_as_PNG_file)

    def to_pdf(self, path, quality=None):
        raise xlwings.XlwingsError("Chart.to_pdf() isn't supported on macOS.")


class Charts(Collection, base_classes.Charts):
    _attr = "chart_objects"
    _kw = kw.chart_object
    _wrap = Chart

    def add(self, left, top, width, height):
        sheet_index = self.parent.xl.entry_index.get()
        return Chart(
            self.parent,
            self.parent.xl.make(
                at=self.parent.book.xl.sheets[sheet_index],
                new=kw.chart_object,
                with_properties={
                    kw.width: width,
                    kw.top: top,
                    kw.left_position: left,
                    kw.height: height,
                },
            ).name.get(),
        )


class Picture(base_classes.Picture):
    def __init__(self, parent, key):
        self._parent = parent
        self.xl = parent.xl.pictures[key]

    @property
    def parent(self):
        return self._parent

    @property
    def api(self):
        return self.xl

    @property
    def name(self):
        return self.xl.name.get()

    @name.setter
    def name(self, value):
        self.xl.name.set(value)

    @property
    def left(self):
        return self.xl.left_position.get()

    @left.setter
    def left(self, value):
        self.xl.left_position.set(value)

    @property
    def top(self):
        return self.xl.top.get()

    @top.setter
    def top(self, value):
        self.xl.top.set(value)

    @property
    def width(self):
        return self.xl.width.get()

    @width.setter
    def width(self, value):
        self.xl.width.set(value)

    @property
    def height(self):
        return self.xl.height.get()

    @height.setter
    def height(self, value):
        self.xl.height.set(value)

    def delete(self):
        self.xl.delete()

    @property
    def lock_aspect_ratio(self):
        return self.xl.lock_aspect_ratio.get()

    @lock_aspect_ratio.setter
    def lock_aspect_ratio(self, value):
        self.xl.lock_aspect_ratio.set(value)

    def update(self, filename):
        return utils.excel_update_picture(self, filename)


class Pictures(Collection, base_classes.Pictures):
    _attr = "pictures"
    _kw = kw.picture
    _wrap = Picture

    def add(
        self,
        filename,
        link_to_file,
        save_with_document,
        left,
        top,
        width,
        height,
        anchor,
    ):
        if anchor:
            top, left = anchor.top, anchor.left

        version = VersionNumber(self.parent.book.app.version)

        if not link_to_file and version >= 15:
            # Office 2016 for Mac is sandboxed. This path seems to work without the
            # need of granting access explicitly.
            xlwings_picture = (
                os.path.expanduser("~")
                + "/Library/Containers/com.microsoft.Excel/Data/xlwings_picture.png"
            )
            shutil.copy2(filename, xlwings_picture)
            filename = xlwings_picture

        sheet_index = self.parent.xl.entry_index.get()
        picture = Picture(
            self.parent,
            self.parent.xl.make(
                at=self.parent.book.xl.sheets[sheet_index],
                new=kw.picture,
                with_properties={
                    kw.file_name: posix_to_hfs_path(filename),
                    kw.link_to_file: link_to_file,
                    kw.save_with_document: save_with_document,
                    kw.width: width,
                    kw.height: height,
                    # Top and left: see below
                    kw.top: 0,
                    kw.left_position: 0,
                },
            ).name.get(),
        )

        # Top and left cause an issue in the make command above
        # if they are not set to 0 when width & height are -1
        picture.top = top if top else 0
        picture.left = left if left else 0

        if not link_to_file and version >= 15:
            os.remove(filename)

        return picture


class Names(base_classes.Names):
    def __init__(self, parent, xl):
        self.parent = parent
        self.xl = xl

    def __call__(self, name_or_index):
        return Name(self.parent, xl=self.xl[name_or_index])

    def contains(self, name_or_index):
        try:
            self.xl[name_or_index].get()
        except appscript.reference.CommandError:
            # TODO: make more specific
            return False
        return True

    def __len__(self):
        named_items = self.xl.get()
        if named_items == kw.missing_value:
            return 0
        else:
            return len(named_items)

    def add(self, name, refers_to):
        return Name(
            self.parent,
            self.parent.xl.make(
                at=self.parent.xl,
                new=kw.named_item,
                with_properties={kw.references: refers_to, kw.name: name},
            ),
        )


class Name(base_classes.Name):
    def __init__(self, parent, xl):
        self.parent = parent
        self.xl = xl

    def delete(self):
        self.xl.delete()

    @property
    def name(self):
        return self.xl.name.get()

    @name.setter
    def name(self, value):
        self.xl.name.set(value)

    @property
    def refers_to(self):
        return self.xl.properties().get(kw.references)

    @refers_to.setter
    def refers_to(self, value):
        self.xl.properties(kw.references).set(value)

    @property
    def refers_to_range(self):
        book = self.parent if isinstance(self.parent, Book) else self.parent.book
        external_address = self.xl.reference_range.get_address(external=True)
        match = re.search(r"\](.*?)'?!(.*)", external_address)
        return Range(Sheet(book, match.group(1)), match.group(2))


class Shapes(Collection):
    _attr = "shapes"
    _kw = kw.shape
    _wrap = Shape


@atexit.register
def cleanup():
    """
    Since AppleScript cannot access Excel while a Macro is running, we have to run the
    Python call in a background process which makes the call return immediately: we
    rely on the StatusBar to give the user feedback.
    This function is triggered when the interpreter exits and runs the CleanUp Macro in
    VBA to show any errors and to reset the StatusBar.
    """
    if is_excel_running():
        # Prevents Excel from reopening
        # if it has been closed manually or never been opened
        for app in Apps():
            try:
                app.xl.run_VB_macro("CleanUp")
            except (CommandError, AttributeError, aem.aemsend.EventError):
                # Excel files initiated from Python don't have the xlwings VBA module
                pass


def posix_to_hfs_path(posix_path):
    """
    Turns a posix path (/Path/file.ext) into an HFS path (Macintosh HD:Path:file.ext)
    """
    dir_name, file_name = os.path.split(posix_path)
    dir_name_hfs = mactypes.Alias(dir_name).hfspath
    return dir_name_hfs + ":" + file_name


def hfs_to_posix_path(hfs_path):
    """
    Turns an HFS path (Macintosh HD:Path:file.ext) into a posix path (/Path/file.ext)
    """
    url = mactypes.convertpathtourl(hfs_path, 1)  # kCFURLHFSPathStyle = 1
    return mactypes.converturltopath(url, 0)  # kCFURLPOSIXPathStyle = 0


def is_excel_running():
    for proc in psutil.process_iter():
        try:
            if proc.name() == "Microsoft Excel":
                return True
        except psutil.NoSuchProcess:
            pass
    return False


# --- constants ---

chart_types_k2s = {
    kw.ThreeD_area: "3d_area",
    kw.ThreeD_area_stacked: "3d_area_stacked",
    kw.ThreeD_area_stacked_100: "3d_area_stacked_100",
    kw.ThreeD_bar_clustered: "3d_bar_clustered",
    kw.ThreeD_bar_stacked: "3d_bar_stacked",
    kw.ThreeD_bar_stacked_100: "3d_bar_stacked_100",
    kw.ThreeD_column: "3d_column",
    kw.ThreeD_column_clustered: "3d_column_clustered",
    kw.ThreeD_column_stacked: "3d_column_stacked",
    kw.ThreeD_column_stacked_100: "3d_column_stacked_100",
    kw.ThreeD_line: "3d_line",
    kw.ThreeD_pie: "3d_pie",
    kw.ThreeD_pie_exploded: "3d_pie_exploded",
    kw.area_chart: "area",
    kw.area_stacked: "area_stacked",
    kw.area_stacked_100: "area_stacked_100",
    kw.bar_clustered: "bar_clustered",
    kw.bar_of_pie: "bar_of_pie",
    kw.bar_stacked: "bar_stacked",
    kw.bar_stacked_100: "bar_stacked_100",
    kw.bubble: "bubble",
    kw.bubble_ThreeD_effect: "bubble_3d_effect",
    kw.column_clustered: "column_clustered",
    kw.column_stacked: "column_stacked",
    kw.column_stacked_100: "column_stacked_100",
    kw.combination_chart: "combination",
    kw.cone_bar_clustered: "cone_bar_clustered",
    kw.cone_bar_stacked: "cone_bar_stacked",
    kw.cone_bar_stacked_100: "cone_bar_stacked_100",
    kw.cone_col: "cone_col",
    kw.cone_column_clustered: "cone_col_clustered",
    kw.cone_column_stacked: "cone_col_stacked",
    kw.cone_column_stacked_100: "cone_col_stacked_100",
    kw.cylinder_bar_clustered: "cylinder_bar_clustered",
    kw.cylinder_bar_stacked: "cylinder_bar_stacked",
    kw.cylinder_bar_stacked_100: "cylinder_bar_stacked_100",
    kw.cylinder_column: "cylinder_col",
    kw.cylinder_column_clustered: "cylinder_col_clustered",
    kw.cylinder_column_stacked: "cylinder_col_stacked",
    kw.cylinder_column_stacked_100: "cylinder_col_stacked_100",
    kw.doughnut: "doughnut",
    kw.doughnut_exploded: "doughnut_exploded",
    kw.line_chart: "line",
    kw.line_markers: "line_markers",
    kw.line_markers_stacked: "line_markers_stacked",
    kw.line_markers_stacked_100: "line_markers_stacked_100",
    kw.line_stacked: "line_stacked",
    kw.line_stacked_100: "line_stacked_100",
    kw.pie_chart: "pie",
    kw.pie_exploded: "pie_exploded",
    kw.pie_of_pie: "pie_of_pie",
    kw.pyramid_bar_clustered: "pyramid_bar_clustered",
    kw.pyramid_bar_stacked: "pyramid_bar_stacked",
    kw.pyramid_bar_stacked_100: "pyramid_bar_stacked_100",
    kw.pyramid_column: "pyramid_col",
    kw.pyramid_column_clustered: "pyramid_col_clustered",
    kw.pyramid_column_stacked: "pyramid_col_stacked",
    kw.pyramid_column_stacked_100: "pyramid_col_stacked_100",
    kw.radar: "radar",
    kw.radar_filled: "radar_filled",
    kw.radar_markers: "radar_markers",
    kw.stock_HLC: "stock_hlc",
    kw.stock_OHLC: "stock_ohlc",
    kw.stock_VHLC: "stock_vhlc",
    kw.stock_VOHLC: "stock_vohlc",
    kw.surface: "surface",
    kw.surface_top_view: "surface_top_view",
    kw.surface_top_view_wireframe: "surface_top_view_wireframe",
    kw.surface_wireframe: "surface_wireframe",
    kw.xy_scatter_lines: "xy_scatter_lines",
    kw.xy_scatter_lines_no_markers: "xy_scatter_lines_no_markers",
    kw.xy_scatter_smooth: "xy_scatter_smooth",
    kw.xy_scatter_smooth_no_markers: "xy_scatter_smooth_no_markers",
    kw.xyscatter: "xy_scatter",
}

chart_types_s2k = {v: k for k, v in chart_types_k2s.items()}

directions_s2k = {
    "d": kw.toward_the_bottom,
    "down": kw.toward_the_bottom,
    "l": kw.toward_the_left,
    "left": kw.toward_the_left,
    "r": kw.toward_the_right,
    "right": kw.toward_the_right,
    "u": kw.toward_the_top,
    "up": kw.toward_the_top,
}

directions_k2s = {
    kw.toward_the_bottom: "down",
    kw.toward_the_left: "left",
    kw.toward_the_right: "right",
    kw.toward_the_top: "up",
}

calculation_k2s = {
    kw.calculation_automatic: "automatic",
    kw.calculation_manual: "manual",
    kw.calculation_semiautomatic: "semiautomatic",
}

calculation_s2k = {v: k for k, v in calculation_k2s.items()}

shape_types_k2s = {
    kw.shape_type_3d_model: "3d_model",
    kw.shape_type_auto: "auto_shape",
    kw.shape_type_callout: "callout",
    kw.shape_type_canvas: "canvas",
    kw.shape_type_chart: "chart",
    kw.shape_type_comment: "comment",
    kw.shape_type_content_application: "content_app",
    kw.shape_type_diagram: "diagram",
    kw.shape_type_free_form: "free_form",
    kw.shape_type_graphic: "graphic",
    kw.shape_type_group: "group",
    kw.shape_type_embedded_OLE_control: "embedded_ole_object",
    kw.shape_type_form_control: "form_control",
    kw.shape_type_line: "line",
    kw.shape_type_linked_3d_model: "linked_3d_model",
    kw.shape_type_linked_graphic: "linked_graphic",
    kw.shape_type_linked_OLE_object: "linked_ole_object",
    kw.shape_type_linked_picture: "linked_picture",
    kw.shape_type_OLE_control: "ole_control_object",
    kw.shape_type_picture: "picture",
    kw.shape_type_place_holder: "placeholder",
    kw.shape_type_web_video: "web_video",
    kw.shape_type_media: "media",
    kw.shape_type_text_box: "text_box",
    kw.shape_type_table: "table",
    kw.shape_type_ink: "ink",
    kw.shape_type_ink_comment: "ink_comment",
    kw.shape_type_unset: "unset",
}

scaling = {
    "scale_from_top_left": kw.scale_from_top_left,
    "scale_from_bottom_right": kw.scale_from_bottom_right,
    "scale_from_middle": kw.scale_from_middle,
}

shape_types_s2k = {v: k for k, v in shape_types_k2s.items()}
