local group = class("core.ui.stackType", require("core.ui.uitype.groupBase"))
local tag = group.getName()

function group:ctor(ui, config)
  group.super.ctor(self, ui, config)
  self._stackIndexes = {}
  self._stackPages = {}
  self._repeatedPageIndex = 1
  self._sortingOrderSpace = config.stackSortingOrderSpace or 10
end

function group:_destroyPage(d, options)
  d.cls:unBind()
  d.cls:destroy(options)
  d.cls:disposeUIBinding()
  self._ui:execMiddleware("onDestroy", d.assetName, d.gameObject)
  if d.cls.loadHandle:IsValid() then
    self._ui:execMiddleware("releaseHandler", d.cls)
  end
end

function group:_toHideCoexist_dontDeactive(index)
  local p = self._stackIndexes[index]
  if not p then
    return
  end
  local page = self._stackPages[p]
  if not page then
    return
  end
  local pageCls = page.cls
  if pageCls then
    pageCls:hide()
    pageCls:hideModules()
  end
  self._ui:dispatch(self._ui.pageEvent.hided, page.pageName)
  page.status = "hided"
  if pageCls then
    pageCls.status = "hided"
    local blendDefinition = pageCls.blendDefinition
    if blendDefinition then
      blendDefinition:HideReset()
      blendDefinition:PlayHide(nil)
    end
  end
  local mode = page.mode or page.config.mode
  if mode == "coexist" then
    self:_toHideCoexist_dontDeactive(index + 1)
  end
end

function group:_toDeactiveCoexist(index)
  local p = self._stackIndexes[index]
  if not p then
    return
  end
  local page = self._stackPages[p]
  if not page then
    return
  end
  local obj = page.gameObject
  if obj and not obj:IsNull() then
    obj:SetActive(false)
  end
  local mode = page.mode or page.config.mode
  if mode == "coexist" then
    self:_toDeactiveCoexist(index + 1)
  end
end

function group:_toBack(index, options, stayActive)
  local pageName = self._stackIndexes[index]
  local page = self._stackPages[pageName]
  if page == nil then
    errorf("[UI.stackType._toBack] nil page: " .. pageName)
    return
  end
  table.remove(self._stackIndexes, index)
  self._stackPages[pageName] = nil
  if page.isPlaceholder then
  else
    if page.cls.status == "shown" then
      page.cls:hide()
      self._ui:dispatch(self._ui.pageEvent.hided, pageName)
    end
    page.cls:close(options)
    page.cls:hideModules(options)
    page.cls:closeModules(options)
    page.status = "closed"
    page.cls.status = "closed"
    if not stayActive then
      page.gameObject:SetActive(false)
      self:_destroyPage(page)
    end
  end
  self._ui:dispatch(self._ui.pageEvent.closed, pageName)
  self:internalOnPageClose(page)
end

function group:_handleOpenRepeat(name)
  if self._stackPages[name] == nil then
    return name
  end
  local pageName = name
  if self._stackPages[pageName].canRepeat and self._stackPages[pageName].canRepeat == true then
    for i = 1, #self._stackIndexes do
      local p = string.sub(self._stackIndexes[i], 1, string.len(pageName))
      if pageName == p then
        self._repeatedPageIndex = self._repeatedPageIndex + 1
        if self._repeatedPageIndex > 99999 then
          self._repeatedPageIndex = 1
        end
        pageName = name .. "#" .. self._repeatedPageIndex
      end
    end
    return pageName
  else
    printf(tag, string.format("<color=red>Open page(%s) repeatedly. The page will not be opened.</color>", pageName))
    return nil
  end
end

function group:_updateSortingOrder(page, pageOrder)
  if page == nil then
    return
  end
  local pageCls = page.cls
  if pageCls then
    pageCls.canvas.sortingOrder = pageOrder * self._sortingOrderSpace
  end
end

function group:_procCameraState()
  local pageName = self:bottomVisiblePage()
  local page = self._stackPages[pageName]
  if page then
    local cfg = page.config
    local displayMode = cfg and cfg.displayMode
    self._ui:execMiddleware("sceneUISwitch", displayMode == "sceneUI")
    self._ui:execMiddleware("handleMainCameraState", displayMode)
  end
end

function group:_doOpen(page, options)
  self._ui:profilerMarkTimeForRecord(page.pageName, "t_doOpenStart")
  page.cls:open(options)
  self:_show(page)
  self._ui:playAudio(self._ui.audioEvent.pageBgm, page.config.windowBGM, true)
  self._ui:dispatch(self._ui.pageEvent.showed, page.pageName)
  self:_procCameraState()
  self._ui:profilerMarkTimeForRecord(page.pageName, "t_doOpenEnd")
end

function group:_show(page)
  self._ui:processAudioInterfaceStatus(page)
  self._ui:playAudio(self._ui.audioEvent.pageState, page.config.windowState)
  page.cls:show()
end

function group:_procOldPageWhenOpen(oldPage, needDeactive, needDestroy)
  if oldPage == nil then
    return
  end
  if oldPage.isPlaceholder then
    return
  end
  local oldPageStatus = oldPage.status
  if needDeactive and (oldPageStatus == "hided" or oldPageStatus == "closed") then
    local oldPageOrder = self:pageOrder(oldPage.pageName)
    if 0 < oldPageOrder then
      local pageCount = #self._stackIndexes
      self:_toDeactiveCoexist(pageCount - oldPageOrder + 1)
    end
  end
  if needDestroy and oldPageStatus == "closed" then
    self:_destroyPage(oldPage)
  end
end

function group:_procPageTransition(page, options, callback, oldPage, needDeactive, needDestroy)
  self._ui:profilerMarkTimeForRecord(page.pageName, "t_preOpenStart")
  table.insert(self._stackIndexes, 1, page.pageName)
  self:_updateSortingOrder(page, #self._stackIndexes)
  if not page.config.dontDisableRaycast then
    self:disableRaycaster()
  end
  page.gameObject:SetActive(true)
  page.cls:openModules(options)
  page.cls:showModules(options)
  page.cls:preOpen(options)
  self._ui:playAudio(self._ui.audioEvent.pageOpen, page.config.windowOpenAudio)
  self._ui:dispatch(self._ui.pageEvent.preOpen, page.pageName)
  page.cls.status = "shown"
  page.status = "shown"
  self._ui:profilerMarkTimeForRecord(page.pageName, "t_preOpenEnd")
  local blendDefinition = page.cls.blendDefinition
  if blendDefinition then
    page.animating = true
    blendDefinition:OpenReset()
    self._ui:profilerMarkTimeForRecord(page.pageName, "t_openAnimStart")
    blendDefinition:PlayOpen(function()
      self._ui:profilerMarkTimeForRecord(page.pageName, "t_openAnimEnd")
      if not page.config.dontDisableRaycast then
        self:enableRaycaster()
      end
      page.animating = false
      self:_procOldPageWhenOpen(oldPage, needDeactive, needDestroy)
      if page.status == "shown" then
        self:_doOpen(page, options)
      end
      if callback ~= nil then
        callback(true)
      end
    end)
  else
    if not page.config.dontDisableRaycast then
      self:enableRaycaster()
    end
    self:_procOldPageWhenOpen(oldPage, needDeactive, needDestroy)
    self:_doOpen(page, options)
    if callback ~= nil then
      callback(true)
    end
  end
end

function group:_toOpen(page, options, callback)
  if #self._stackIndexes <= 0 then
    self:_procPageTransition(page, options, callback, nil)
    return
  end
  local p = self._stackIndexes[1]
  local oldPage = self._stackPages[p]
  if not oldPage or not oldPage.isPlaceholder and not oldPage.cls then
    errorf(string.format("[toOpen:%s] old page is nil or not ready: %s", page.pageName, p))
    self:_procPageTransition(page, options, callback, nil)
    return
  end
  local mode
  if options and options.mode then
    mode = options.mode
  else
    mode = page.config.mode
  end
  local swiftly = false
  if mode == "replaceSwiftly" then
    mode = "replace"
    swiftly = true
  end
  if mode == nil then
    mode = "exclusive"
  end
  page.mode = mode
  if mode == "replace" then
    self:_toBack(1, nil, true)
    local blendDefinition = oldPage.cls and oldPage.cls.blendDefinition
    if blendDefinition then
      local oldPageCurOrder = oldPage.cls.canvas.sortingOrder
      oldPage.cls.canvas.sortingOrder = oldPageCurOrder - 1
      oldPage.animating = true
      blendDefinition:CloseReset()
      self._ui:profilerMarkTimeForRecord(page.pageName, "t_closeAnimStart")
      blendDefinition:PlayClose(function()
        self._ui:profilerMarkTimeForRecord(page.pageName, "t_closeAnimEnd")
        oldPage.animating = false
        if not swiftly then
          self:_procPageTransition(page, options, callback, oldPage, true, true)
        end
      end)
      if swiftly then
        self:_procPageTransition(page, options, callback, oldPage, true, true)
      end
    else
      self:_procPageTransition(page, options, callback, oldPage, true, true)
    end
  elseif mode == "coexist" then
    self:_procPageTransition(page, options, callback, oldPage, false, false)
  elseif mode == "exclusive" then
    self:_toHideCoexist_dontDeactive(1)
    self:_procPageTransition(page, options, callback, oldPage, true, false)
  else
    errorf("unsupported mode: " .. tostring(mode))
    self:_procPageTransition(page, options, callback, oldPage, true, false)
  end
end

function group:_toShowWhenClose(index)
  local pageName = self._stackIndexes[index]
  if pageName == nil then
    return
  end
  local page = self._stackPages[pageName]
  if not page then
    errorf("unkown bug stackPages dont has this page:" .. pageName)
    return
  end
  if page.status == "shown" then
    return
  end
  local mode = page.mode or page.config.mode
  if mode == "coexist" then
    self:_toShowWhenClose(index + 1)
  end
  page.gameObject:SetActive(true)
  self:_procCameraState()
  page.status = "shown"
  page.cls.status = "shown"
  local blendDefinition = page.cls.blendDefinition
  if blendDefinition then
    blendDefinition:ShowReset()
    blendDefinition:PlayShow(nil)
  end
  page.cls:showModules()
  self:_show(page)
  self._ui:playAudio(self._ui.audioEvent.pageBgm, page.config.windowBGM, true)
  self._ui:dispatch(self._ui.pageEvent.showed, page.pageName)
end

function group:_onNewRestoredPage(page, options)
  if page == nil then
    return
  end
  self._stackPages[page.pageName] = page
  self:_updateSortingOrder(page, self:pageOrder(page.pageName))
  page.gameObject:SetActive(true)
  page.cls:openModules(options)
  page.cls:showModules()
  page.cls:preOpen(options)
  self._ui:dispatch(self._ui.pageEvent.preOpen, page.pageName)
  page.cls.status = "shown"
  page.status = "shown"
  local blendDefinition = page.cls.blendDefinition
  if blendDefinition then
    blendDefinition:PlayOpen(nil, 0)
  end
  page.cls:open(options)
  self:_procCameraState()
  self:_show(page)
  self._ui:playAudio(self._ui.audioEvent.pageBgm, page.config.windowBGM, true)
  self._ui:dispatch(self._ui.pageEvent.showed, page.pageName)
end

function group:_toRestoreWhenClose(index)
  local pageName = self._stackIndexes[index]
  if pageName == nil then
    return
  end
  local pagePlaceholder = self._stackPages[pageName]
  if not pagePlaceholder then
    errorf("stackPages dont has this page:" .. pageName)
    return
  end
  if not pagePlaceholder.isPlaceholder then
    errorf("isPlaceholder should be true: " .. pageName)
    return
  end
  local mode = pagePlaceholder.mode or pagePlaceholder.config.mode
  if mode == "coexist" then
    self:_toRestoreWhenClose(index + 1)
  end
  local options = pagePlaceholder.options
  self._ui:newPage(pageName, options, function(page)
    page.mode = pagePlaceholder.mode
    self:_onNewRestoredPage(page, options)
  end)
end

function group:open(name, options, callback)
  local pageName = self:_handleOpenRepeat(name)
  if not pageName then
    if callback ~= nil then
      callback(false)
    end
    return
  end
  local newPageFailed = false
  local page = self._ui:newPage(pageName, options, function(p)
    newPageFailed = p == nil
    self._stackPages[pageName] = p
    if p then
      self:_toOpen(p, options, callback)
    elseif callback ~= nil then
      callback(false)
    end
  end)
  if not newPageFailed then
    self._stackPages[pageName] = page
  end
end

function group:close(name, options, callback)
  if not string.isEmpty(name) and name ~= self._stackIndexes[1] then
    local pullingIdx, pullingName
    for i = 2, #self._stackIndexes do
      local pName = self._stackIndexes[i]
      if pName == name then
        pullingIdx = i
        pullingName = name
        break
      end
    end
    if pullingIdx ~= nil then
      self:_toBack(pullingIdx, options)
      for i = 1, pullingIdx - 1 do
        local pName = self._stackIndexes[i]
        local p = self._stackPages[pName]
        self:_updateSortingOrder(p, self:pageOrder(pName))
      end
    else
      warnf(tag, "The stack doesn't contain the closing page: " .. tostring(name))
    end
    if callback ~= nil then
      callback(pullingName)
    end
    return
  end
  local backAll = false
  local backRoot = false
  local backTarName
  if options == nil then
    backTarName = self._stackIndexes[2]
    if not backTarName then
      backAll = true
    end
  else
    backAll = options.backAll
    if backAll then
      backTarName = nil
    else
      backRoot = options.backRoot
      local curPageCount = #self._stackIndexes
      if backRoot then
        backTarName = self._stackIndexes[curPageCount]
      else
        backTarName = options.backName
        if not backTarName then
          local backCount = tonumber(options.backCount)
          if backCount ~= nil then
            backTarName = self._stackIndexes[backCount + 1]
          else
            backTarName = self._stackIndexes[2]
          end
        end
      end
    end
  end
  if not backAll and not backTarName then
    errorf("\233\157\158backAll\231\154\132\230\131\133\229\134\181\228\184\139backTarName\228\184\141\229\186\148\232\175\165\228\184\186\231\169\186\239\188\129")
    if callback then
      callback(nil)
    end
    return
  end
  local topPageName = self._stackIndexes[1]
  if not topPageName or topPageName == backTarName then
    if callback ~= nil then
      callback(nil)
    end
    return
  end
  local topPage = self._stackPages[topPageName]
  self:_toBack(1, options, true)
  while true do
    local pName = self._stackIndexes[1]
    if pName == nil or not backAll and self:getPage(backTarName) == nil or pName == backTarName then
      break
    end
    self:_toBack(1, options, false)
  end
  local newpageName = self._stackIndexes[1]
  if newpageName ~= nil then
    local newpage = self._stackPages[newpageName]
    if newpage then
      if newpage.status ~= "shown" then
        if newpage.isPlaceholder then
          self:_toRestoreWhenClose(1)
        else
          self:_toShowWhenClose(1)
        end
      end
    else
      printf(tag, "self._stackPages doesn't contain this page:" .. newpageName)
    end
  end
  local blendDefinition = topPage.cls.blendDefinition
  if blendDefinition then
    topPage.animating = true
    blendDefinition:CloseReset()
    blendDefinition:PlayClose(function()
      topPage.animating = false
      topPage.gameObject:SetActive(false)
      self:_destroyPage(topPage)
      if callback ~= nil then
        callback(topPageName)
      end
    end)
  else
    topPage.gameObject:SetActive(false)
    self:_destroyPage(topPage)
    if callback ~= nil then
      callback(topPageName)
    end
  end
end

function group:show(pageName, options)
  errorf("\230\160\136\231\177\187\229\158\139\231\149\140\233\157\162\228\184\141\229\186\148\232\175\165\231\148\168\232\191\153\228\184\170\230\142\165\229\143\163\230\157\165show, pageName:" .. pageName)
end

function group:hide(pageName, options)
  errorf("\230\160\136\231\177\187\229\158\139\231\149\140\233\157\162\228\184\141\229\186\148\232\175\165\231\148\168\232\191\153\228\184\170\230\142\165\229\143\163\230\157\165hide, pageName:" .. pageName)
end

function group:update()
  for _, v in ipairs(self._stackIndexes) do
    local p = self._stackPages[v]
    if p ~= nil and p.status == "shown" then
      local pcls = p.cls
      if pcls and pcls.update then
        pcls:update()
      end
    end
  end
end

function group:info()
  local result = "stackPages:"
  for i, v in ipairs(self._stackIndexes) do
    result = result .. v .. ","
  end
  result = result .. "\n"
  return result
end

function group:pageInfo(pageName)
  local page = self._stackPages[pageName]
  if page == nil then
    return nil
  else
    return table.tostring(page, 20)
  end
end

function group:currentPage()
  return self._stackIndexes[1]
end

function group:rootPage()
  return self._stackIndexes[#self._stackIndexes]
end

function group:pageOrder(pageName)
  local order = 0
  for i = #self._stackIndexes, 1, -1 do
    order = order + 1
    local n = self._stackIndexes[i]
    if n == pageName then
      return order
    end
  end
  return -1
end

function group:bottomVisiblePage()
  local ret
  for i, pageName in ipairs(self._stackIndexes) do
    local page = self._stackPages[pageName]
    if not page then
      errorf(string.format("cannot find %s(index:%d) in stackPages.", pageName, i))
      return
    end
    local pageCfg = page.config
    if pageCfg.mode ~= "coexist" then
      ret = pageName
      break
    end
  end
  return ret
end

function group:internalEnableRaycaster()
  for i, v in pairs(self._stackPages) do
    if v.status == "shown" then
      self._ui:setPageRaycasterEnabled(v, true)
    end
  end
  self._ui:execMiddleware("enableAllPlayerInput")
end

function group:internalDisableRaycaster()
  for i, v in pairs(self._stackPages) do
    if v.status == "shown" then
      self._ui:setPageRaycasterEnabled(v, false)
    end
  end
  self._ui:execMiddleware("disableAllPlayerInput")
end

function group:destroy(pageName, options)
  if #self._stackIndexes <= 0 then
    return
  end
  local page = self._stackPages[pageName]
  if page then
    for i, v in ipairs(self._stackIndexes) do
      if self._stackIndexes[i] == pageName then
        table.remove(self._stackIndexes, i)
      end
    end
    self:_destroyPage(page, options)
  end
end

function group:getPage(pageName)
  local page = self._stackPages[pageName]
  if page ~= nil then
    return page
  end
  return nil
end

function group:getAllPages()
  return self._stackPages
end

function group:refreshAllPages()
  for _, page in pairs(self._stackPages) do
    if page.cls then
      page.cls:refresh()
      page.cls:refreshModules()
    end
  end
end

function group:clearAllPages()
  self:close("", {
    backCount = #self._stackIndexes - 1
  })
end

return group
