class AcquisitionImage(object):
    def __init__(self, filename, mosmask=None, mdfdir=None):
        self.ad = AstroData(filename)
        self.mosmask = mosmask
        self.mdfdir = mdfdir

        # Determine extension
        nsci = len(self.ad)
        debug("...nsci = ", nsci)
            
        if nsci > 1:
            l_sci_ext = 1 
        else:
            l_sci_ext = 0

        debug("...using extension [" + str(l_sci_ext) + "]")

        overscan_dv = self.ad[l_sci_ext].overscan_section()

        if self.is_mos_mode():
            self.box_coords = parse_box_coords(self, self.get_mdf_filename())
            self.box_mosaic = BoxMosaic(self, self.box_coords)
            self.scidata = self.box_mosaic.get_science_data()
        elif self.is_new_gmosn_ccd():
            # tile the 2 center parts of the new GMOS image
            self.scidata = gmultiamp(self.ad)
        elif not overscan_dv.is_none():
            # remove the overscan so we don't have to take it into account when guessing the slit location
            self.scidata = subtract_overscan(self.ad[l_sci_ext])

            # it still affects the center of rotation however
            ox1, ox2, oy1, oy2 = overscan_dv.as_list()
            correction = np.array([ox2 - ox1, 0])
            center = self.get_binned_data_center() - correction
            self.fieldcenter = center * self.detector_y_bin()
        else:
            self.scidata = self.ad[l_sci_ext].data

    @cache
    def instrument(self):
        return str(self.ad.instrument())

    def is_new_gmosn_ccd(self):
        header = self.ad.phu.header
        if "DETECTOR" not in header:
            return False
        
        if header["DETECTOR"] == "GMOS + e2v DD CCD42-90":
            return True
        return False

    def get_science_data(self):
        assert self.scidata is not None
        return self.scidata

    @cache
    def unbinned_pixel_scale(self):
        return float(self.ad.pixel_scale()) / self.detector_y_bin()

    @cache
    def binned_pixel_scale(self):
        return float(self.ad.pixel_scale())

    def _check_binning(self):
        if int(self.ad.detector_x_bin()) != int(self.ad.detector_y_bin()):
            error("ERROR: incorrect binning!")
            error("Sorry about that, better luck next time.")
            sys.exit(1)

    @cache
    def detector_x_bin(self):
        self._check_binning()
        return int(self.ad.detector_x_bin())

    @cache
    def detector_y_bin(self):
        self._check_binning()
        return int(self.ad.detector_y_bin())

    @cache
    def program_id(self):
        return str(self.ad.program_id())

    @cache
    def observation_id(self):
        return str(self.ad.observation_id())

    @cache
    def saturation_level(self):
        dv = self.ad.saturation_level()
        return min(dv.as_list())

    @cache
    def focal_plane_mask(self):
        return str(self.ad.focal_plane_mask())

    @cache
    def grating(self):
        return str(self.ad.grating())

    def get_detector_size(self):
        # mos mode acquisitions don't necessarily have the entire
        # field of view in their data sections, so we have to rely on
        # other tricks to figure out the center of rotation.

        detsize = self.ad.phu_get_key_value("DETSIZE")
        xmin, xdim, ymin, ydim = extract_dimensions(detsize)
        
        # adjust for chip gaps
        nccds = int(self.ad.phu_get_key_value("NCCDS"))
        xdim += ((nccds - 1) * _obtain_unbinned_arraygap(self.ad))

        # adjust for un-illuminated pixels
        if self.is_gmos():
            ydim -= 36 # magic number that should be replaced with a lookup table later

        return xdim, ydim
        

    def get_field_center(self):
        """ The center of rotation in pixels. """
        if hasattr(self, "fieldcenter"):
            return self.fieldcenter

        if self.is_mos_mode():
            xdim, ydim = self.get_detector_size()
            return np.array([float(xdim) / 2.0, float(ydim) / 2.0])

        return self.get_data_center()

    def get_data_center(self):
        ydim, xdim = self.get_science_data().shape
        return np.array([float(xdim) / 2.0, float(ydim) / 2.0]) * self.detector_y_bin()

    def get_binned_data_center(self):
        return self.get_data_center() / self.detector_y_bin()

    def set_goal_center(self, center):
        self.goal_center = np.array(center)

    def get_goal_center(self):
        default = self.get_data_center()
        return getattr(self, "goal_center", default)

    def set_binned_custom_center(self, center):
        self.set_binned_goal_center(center)
        self.custom_center = True

    def has_custom_center(self):
        return getattr(self, "custom_center", False)

    def get_binned_goal_center(self):
        return self.get_goal_center() / self.detector_y_bin()

    def set_binned_goal_center(self, center):
        center = np.array(center) * self.detector_y_bin()
        self.set_goal_center(center)

    def get_mask_width(self):
        debug("...finding slit dimensions...")
        
        slitxbin = self.detector_x_bin()
        slitybin = self.detector_y_bin()
        debug("...slit image binning = ", slitxbin, " x ", slitybin)
        if slitxbin > 1 or slitybin > 1:
            warning("! WARNING: Slit image is binned " + slitxbin + " x " + slitybin)

        slitmask = self.focal_plane_mask()
        return float(slitmask.replace("arcsec", ""))

    def get_mask_width_in_pixels(self):
        return self.get_mask_width() / self.unbinned_pixel_scale()

    def get_slit_size_in_pixels(self):
        xsize = self.get_mask_width_in_pixels()
        ysize = self.get_science_data().shape[0]
        return xsize, ysize

    def get_expected_slit_tilt(self):
        if self.is_gmos():
            return 0.0

        error("Instrument is not supported, need to know an expected slit tilt")
        sys.exit(1)

    @property
    def phu(self):
        return self.ad.phu

    @property
    def filename(self):
        return self.ad.filename

    def get_program_id_parts(self):
        gemprgid = str(self.ad.program_id())
        parts = gemprgid.split("-")
        if len(parts) != 4:
            msg = "Cannot parse program id '%s'" % gemprgid
            error(msg)
            raise ValueError(msg)
        observatory, semester, prgtype, queuenum = parts
        return observatory, semester, prgtype, int(queuenum)

    def get_semester(self):
        """ Return something in the form of '2006B' """
        observatory, semester, prgtype, queuenum = self.get_program_id_parts()
        return semester

    def get_observatory_prefix(self):
        """ Return something in the form of 'GN' """
        observatory, semester, prgtype, queuenum = self.get_program_id_parts()
        return observatory

    def is_mos_mode(self):
        return self.mosmask is not None or self.has_mos_mask()

    @cache
    def has_mos_mask(self):
        if not self.is_gmos() and not self.is_f2():
            return False

        maskname = self.focal_plane_mask()
        if ("Q" in maskname or   # Queue program
            "C" in maskname or   # Classical program
            "D" in maskname or   # DD program
            "V" in maskname):    # SV program

            xbin = self.detector_x_bin()
            ybin = self.detector_y_bin()
            if xbin != 1 or ybin != 1:
                error ("MOS acquisition image binning must be 1x1, found %ix%i binning." % (xbin, ybin))
                clean()

            return True
        return False

    def has_mask_in_beam(self):
        maskname = self.focal_plane_mask().lower()
        if "imag" in maskname:
            return False

        slitmask = self.focal_plane_mask()    
        if self.is_gmos() and "arcsec" in slitmask:
            return True
    
        if self.is_gnirs() and "arcsec" in slitmask:
            acqmir = slitimage_ad.phu.header["ACQMIR"]
            debug("...acqmir = ", acqmir)
            if acqmir == "In":
                return True
    
        if self.is_niri() and "cam" not in slitmask:
            return True
    
        if self.is_f2() and "slit" in slitmask:
            return True

        if self.is_f2() and "mos" in slitmask:
            return True

        return self.has_mos_mask()

    def get_mdf_filename(self):
        if hasattr(self, "mdffile"):
            return self.mdffile

        self.mdffile = self._get_mdf_filename()
        return self.mdffile

    def _get_mdf_filename(self):
        if self.mosmask is not None:
            mdffile = self.mosmask

            # Expand the MOS mask number
            if is_number(self.mosmask):
                observatory, semester, prgtype, queuenum = self.get_program_id_parts()
                mdffile = "%s%s%s%03i-%02i" % (observatory, semester, prgtype, queuenum, int(self.mosmask))
                debug("...mosmask =", mdffile)
        else:
            mdffile = self.focal_plane_mask()

        mdffile = fits_filename(mdffile)

        #-----------------------------------------------------------------------
        # Start searching around willy nilly for the MDF file
        if os.path.exists(mdffile):
            return mdffile

        # note, the order in which directories are added to this list gives priority
        dirs = []
        if self.mdfdir is not None:
            dirs.append(self.mdfdir)

        dname = os.path.dirname(self.filename)
        if dname and dname != ".":
            dirs.append(dname)

        dirs.append(os.getcwd())

        # search through semester directories as well
        semester_dir = self.get_observatory_prefix() + self.get_semester()
        directories_to_search = []
        for dname in dirs:
            directories_to_search.append(dname)
            dname = os.path.join(dname, semester_dir)
            directories_to_search.append(dname)

        # now search through the directories
        for dname in directories_to_search:
            fname = os.path.join(dname, mdffile)
            debug("...trying", fname)
            if os.path.exists(fname):
                return fname

        raise ValueError("Unable to find MDF file named '%s'" % mdffile)

    def get_num_mos_boxes(self):
        return self.box_mosaic.get_num_mos_boxes()

    def get_mos_boxes(self):
        return self.box_mosaic.get_boxes()

    def get_mos_box_borders(self):
        for border in self.box_mosaic.get_box_borders():
            yield border

    @cache
    def get_min_slitsize(self):
        mdffile_ad = AstroData(self.get_mdf_filename())

        xsize = Ellipsis
        ysize = Ellipsis
        for row in mdffile_ad["MDF"].data:
            # select the alignment boxes, designated by priority 0
            if row["priority"] not in ["1", "2", "3"]:
                continue

            xsize = min(xsize, row["slitsize_x"])
            ysize = min(ysize, row["slitsize_y"])

        return xsize, ysize

    def get_extensions(self):
        for ext in self.ad:
            yield ext

    def _get_lazy_detector_section_finder(self):
        if not hasattr(self, "detsec_finder"):
            self.detsec_finder = DetectorSectionFinder(self)
        return self.detsec_finder

    def get_box_size(self):
        return self._get_lazy_detector_section_finder().get_box_size()

    def find_detector_section(self, point):
        return self._get_lazy_detector_section_finder().find_detector_section(point)

    def get_full_field_of_view(self):
        return self._get_lazy_detector_section_finder().get_full_field_of_view()

    def is_altair(self):
        aofold = self.ad.phu.header["AOFOLD"]
        if aofold == "IN":
            return True
        return False

    def is_south_port(self):
        inportnum = int(self.ad.phu.header["INPORT"])
        if inportnum == 1:
            return True
        return False

    def is_type(self, typename):
        return self.ad.is_type(typename)

    def is_gmos(self):
        return self.is_type("GMOS")
    
    def is_gmosn(self):
        return self.is_type("GMOS_N")

    def is_gmoss(self):
        return self.is_type("GMOS_S")

    def is_gnirs(self):
        return self.is_type("GNIRS")

    def is_f2(self):
        return self.is_type("F2")

    def is_nifs(self):
        return self.is_type("NIFS")

    def is_niri(self):
        return self.is_type("NIRI")