:bug: Add contextElement attribute to 'all' arcs.

jsoniq version "1.0";
(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for manipulating hypercubes.

 :
 :
A hypercube provides a multi-dimensional structure to organize facts.

 :
 :
The dimensions of a hypercube correspond to XBRL aspects (concepts, entities, periods,
 : units, further XBRL dimensions). An XBRL hypercube only is made of XBRL dimensions, however
 : including the other aspects as well is very useful in the context of NOLAP, so that 28.io
 : does so.

 :
 :
28.io introduces the notion of default hypercube. A default hypercube is implicitly added
 : to each component, and only contains the concept, entity, period and unit aspects.
 : In other words, the default hypercube
 : of a component contains all these facts that do not have any XBRL dimensions.

 :
 :
With this module, you can retrieve all hypercubes contained in a component. You can
 : retrieve all facts contained in a hypercube (default dimension values are processed automatically),
 : either in raw form, or organized as a (2D) fact table. You can also populate a network (for example,
 : a presentation network) with the facts contained in a hypercube.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace schema = "http://zorba.io/modules/schema";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";

declare namespace xbrli = "http://www.xbrl.org/2003/instance";
declare namespace xbrldt = "http://xbrl.org/2005/xbrldt";
declare namespace xlink = "http://www.w3.org/1999/xlink";
declare namespace link = "http://www.xbrl.org/2003/linkbase";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Joker for all archives.
 :)
declare variable $hypercubes:ALL_ARCHIVES as boolean := true;

(:~
 :
Retrieves all hypercubes in the supplied components.

 :
 : @param $components a sequence of components.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components($components as object*) as object*
{
  $components.Hypercubes ! values($$)
};

(:~
 :
Retrieves all hypercubes in the supplied components and
 : with the given names.

 :
 : @param $components a sequence of components.
 : @param $names a sequence of names.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components(
    $components as object*,
    $names as string*
) as object*
{
  $names ! $components.Hypercubes.($$)
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object
)
as object*
{
  facts:facts-for({Hypercube: $hypercube})
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object,
    $options as object?
)
as object*
{
  let $options as object := facts:merge-objects(
    { Hypercube: $hypercube },
    $options,
    true (: parameter has higher priority than option :)
  )
  return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*
)
as object*
{
  hypercubes:facts-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*,
    $options as object?
)
as item* (: object* :)
{
    if (exists($options.facts-for-archives-and-concepts))
    then
      let $options as object :=
        facts:merge-objects(
          { Hypercube: $hypercube },
          $options,
          true (: parameter has higher priority than option :)
        )
      let $facts-for-archives-and-concepts := $options.facts-for-archives-and-concepts
      let $concepts as string* :=
          distinct-values(descendant-objects($hypercube.Aspects.$facts:CONCEPT.Members).Name)
      let $concepts := if (exists($concepts))
                   then $concepts
                   else $facts:ALL_OF_THEM
      return
        $facts-for-archives-and-concepts(
        $archives, $concepts, $options)
    else
      let $options as object :=
        facts:merge-objects(
          {
            Hypercube: $hypercube,
            Filter: { $facts:ASPECTS : { $facts:ARCHIVE : [ archives:aid($archives) ] }}
          },
          $options,
          true (: parameter has higher priority than option :)
        )
      return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*) as array
{
  hypercubes:fact-table-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*,
  $options as object?) as array
{
  hypercubes:fact-table-for-hypercube-and-facts(
    $hypercube,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
Utility function to convert a sequence of facts to a fact table
 : given a hypercube. It is not recommended to use this function directly.
 : Use hypercubes:fact-table-for-hypercube instead.

 :
 :
 : @param $hypercube a hypercube.
 : @param $facts a sequence of facts that were retrieved with the hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube-and-facts(
  $hypercube as object,
  $facts as object*) as array
{
  let $dimensions := distinct-values((
      $facts:CONCEPT,
      $facts:ENTITY,
      $facts:PERIOD,
      $facts:UNIT,
      keys($hypercube.Aspects)))
  return [
    [ $dimensions, "Value" ],
    for $fact in $facts
    return [
      $dimensions ! (let $member := $fact.Aspects.($$)
                     return if(exists($member))
                            then $member
                            else null),
      $fact.Value
    ]
  ]
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*) as object*
{
  hypercubes:populate-networks-with-facts($networks, $hypercube, $archives, {})
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*,
  $options as object?) as object*
{
  hypercubes:populate-networks-with-retrieved-facts(
    $networks,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
A utility function that populates networks with facts against concepts that belong
 : to them. It is not recommended to use this function directly.
 : Use hypercubes:populate-networks-with-retrieved-facts.

 :
 : @param $networks networks.
 : @param $facts a sequence of facts.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-retrieved-facts(
  $networks as object*,
  $facts as object*) as object*
{
  let $fact-table-by-concepts := {|
    for $f in $facts
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := $networks
    modify
      for $concept in descendant-objects($result.Trees)[exists($$.Name)]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited.

 :
 : @return dimensionless hypercube instantiation.
 :)
declare function hypercubes:dimensionless-hypercube() as object
{
  hypercubes:dimensionless-hypercube(())
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited, however
 :    you can restrict it using the options. If you need more
 :    XBRL dimensions, use the user-defined-hypercube function instead.

 :
 : @return dimensionless hypercube instantiation.
 : @param $options: additional options among which:
 : - Concepts: an array of concept names to include in the hypercube.
 : - Periods: an array of periods to include in the hypercube.
 : - Entities: an array of EIDs to include in the hypercube.
 : - Units: an array of units to include in the hypercube.
 :)
declare function hypercubes:dimensionless-hypercube($options as object?) as object
{
  hypercubes:user-defined-hypercube(
    {|
      if(exists($options.Concepts))
      then { "xbrl:Concept" : { Domain: $options.Concepts } }
      else (),
      if(exists($options.Entities))
      then { "xbrl:Entity" : { Domain: [ entities:eid($options.Entities[]) ] } }
      else (),
      if(exists($options.Periods))
      then { "xbrl:Period" : { Domain: $options.Periods } }
      else (),
      if(exists($options.Units))
      then { "xbrl:Unit" : { Domain: $options.Units } }
      else ()
    |}
  )
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?) as object
{
    hypercubes:user-defined-hypercube($dimensions, ())
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 : @param $options: an object with two options: Name (default: xbrl:UserDefinedHypercube) and Label
 : (default: User-defined Hypercube).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?, $options as object?) as object
{
  {
    "Name" : ($options.Name, "xbrl:UserDefinedHypercube")[1],
    "Label" : ($options.Label, "User-defined Hypercube")[1],
    "IsOpen" : boolean($options.IsOpen),
    "Aspects" : {|
      for $dimension-name as string in distinct-values((
          "xbrl:Concept",
          "xbrl:Entity",
          "xbrl:Period",
          "xbrl:Unit",
          keys($dimensions)
      ))
      let $dimension-spec as object? := $dimensions.$dimension-name
      let $dimension-type as string? := $dimension-spec.Type
      let $dimension-domain as array? := $dimension-spec.Domain
      let $dimension-default as atomic? := $dimension-spec.Default
      let $domain-name := $dimension-name || "Domain"
      return {
        $dimension-name : {|
          (: Name :)
          {
            "Name" : $dimension-name
          },

          (: Label :)
          switch($dimension-name)
          case "xbrl:Concept" return {
            "Label" : "XBRL Concept Dimension"
          }
          case "xbrl:Entity" return {
            "Label" : "XBRL Entity Dimension"
          }
          case "xbrl:Period" return {
            "Label" : "XBRL Period Dimension"
          }
          case "xbrl:Unit" return {
            "Label" : "XBRL Unit Dimension"
          }
          default return (),

          (: Default value :)
          switch(true)
          case exists($dimension-default) return {
            "Default" :
              hypercubes:check-type-and-return($dimension-default,
                                               ($dimension-type, "string")[1])
          }
          case $dimension-name eq "xbrl:Unit" return {
            "Default" : "xbrl:NonNumeric"
          }
          default return (),

          (: Explicit vs. Typed :)
          {
            "Kind" : if(exists($dimension-type))
                     then "TypedDimension"
                     else "ExplicitDimension"
          },

          (: Type :)
          { "Type": $dimension-type }[exists($dimension-type)],

          (: Domain :)
          if (empty($dimension-type))
          then
          {
            "Members" : [
              for $dimension-member in distinct-values($dimension-domain[])
              return {
                  Name: hypercubes:check-type-and-return($dimension-member, "string")
              }
            ]
          }[exists($dimension-domain)]
          else {
            "DomainRestriction" : {
              "Name" : $domain-name,
              "Label" : $dimension-name || " Domain",
              "Enumeration" : [ hypercubes:check-type-and-return($dimension-domain[], $dimension-type) ]
            }
          }[exists($dimension-domain)]

        |}
      }
    |}
  }
};

declare %private function hypercubes:check-type-and-return(
    $dimension-members as atomic*,
    $dimension-type as string) as atomic*
{
  for $dimension-member as atomic in $dimension-members
  let $member-type as string := local-name-from-QName(schema:schema-type($dimension-member))
  let $expected-type as string := local-name-from-QName(xs:QName($dimension-type))
  return
  if (not $dimension-member instance of null
      and $member-type ne $dimension-type
      and ($member-type ne "int" or $expected-type ne "integer")
      )
  then error(
        xs:QName("hypercubes:invalid-type"),
        schema:schema-type($dimension-member) ||
          ": type doesn't match expected type (" || $dimension-type || ")")
  else $dimension-member
};

declare function hypercubes:modify-hypercube(
    $hypercube as object,
    $options as object?) as object
{
    let $options :=
    {|
        for $dimension in keys(($hypercube.Aspects, $options))
        let $hypercube-metadata := $hypercube.Aspects.$dimension
        let $new-metadata := $options.$dimension
        return {
            $dimension : if(exists($new-metadata))
            then
                $new-metadata
            else {|
                { Type : $hypercube.Aspects.$dimension.Type }[$hypercube-metadata.Kind eq "TypedDimension"],
                let $hypercube-domain :=
                  (
                     descendant-objects($hypercube-metadata.Members).Name,
                     jn:flatten($hypercube-metadata.DomainRestriction.Enumeration)
                  )
                where exists($hypercube-domain)
                return { Domain : [ distinct-values($hypercube-domain) ] },
                let $hypercube-default := $hypercube-metadata.Default
                where exists($hypercube-default)
                return { Default : $hypercube-default }
            |}
        }
    |}
    return hypercubes:user-defined-hypercube(
        $options,
        {
            Name: $hypercube.Name,
            Label: $hypercube.Label || " (with filter overrides)"
        }
    )
};

declare function hypercubes:merge($hypercubes as object*) as object
{
    hypercubes:user-defined-hypercube({|
        for $aspect in keys($hypercubes.Aspects)
        let $default := distinct-values($hypercubes.Aspects.$aspect.Default)
        let $domain := distinct-values((
            descendant-objects($hypercubes.Aspects.$aspect.Members).Name,
            $hypercubes.Aspects.$aspect.DomainRestriction.Enumeration[]
        ))
        let $kind as string? := distinct-values($hypercubes.Aspects.$aspect.Kind)
        let $type as string? := distinct-values($hypercubes.Aspects.$aspect.Type)
        return {
            $aspect : {|
                { Default: $default}[exists($default)],
                { Type : $type }[$kind eq "TypedDimension"],
                { Domain: [ distinct-values(($domain, $default)) ]}[exists($domain)]
            |}
        }
    |})
};

(:~
 :
Transforms a hypercube network - all, hypercube-dimension, dimension-default,
 :    dimension-domain, domain-member (except primary items).

 :
 : @param $hypercube the hypercube to transform.
 :
 : @return the XBRL locators and arcs to be put into an extended link.
 :)
declare function hypercubes:to-xbrl(
    $hypercube as object,
    $component as object,
    $taxonomyName as string
) as element()*
{
  let $hypercubeID as string := replace($hypercube.Name, ":", "_")
  let $hypercubeLocator as element() := <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$hypercubeID}" xlink:label="{$hypercubeID}" xlink:title="{normalize-space($hypercube.Label)}" />
  return (
    (: primary items -> hypercube :)
    for $rootConcept at $pos in $hypercube.Aspects."xbrl:Concept".Members[]
    let $order as decimal :=
        if(empty($rootConcept.Order) or $rootConcept.Order eq null)
        then 1
        else $rootConcept.Order
    let $fromID as string := replace($rootConcept.Name, ":", "_")
    let $fromLabel as string := $fromID || "_" || $pos
    return (
      <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$fromID}" xlink:label="{$fromLabel}" xlink:title="{normalize-space($rootConcept.Label)}" />,
      <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/all" xlink:from="{$fromLabel}" xlink:to="{$hypercubeID}" use="optional" order="{$order}" xbrldt:contextElement="segment" />
    ),
    $hypercubeLocator,

    (: hypercube -> dimension :)
    for $dimension at $pos in values($hypercube.Aspects)[not($$.Name = ("xbrl:Concept", "xbrl:Period", "xbrl:Unit", "xbrl:Entity"))]
    let $dimensionID as string := replace($dimension.Name, ":", "_")
    let $dimensionLabel as string := $dimensionID || "_" || $pos
    let $default as string? := $dimension.Default
    return (
      <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/hypercube-dimension" xlink:from="{$hypercubeID}" xlink:to="{$dimensionLabel}" use="optional" order="{$pos}" />,
      <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$dimensionID}" xlink:label="{$dimensionLabel}" xlink:title="{normalize-space($dimension.Label)}" />,

      (: dimension -> default :)
      if(exists($default))
      then
        let $defaultID as string := replace($default, ":", "_")
        let $defaultLabel as string := $defaultID || "_" || $pos
        return (
          <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/dimension-default" xlink:from="{$dimensionLabel}" xlink:to="{$defaultLabel}" use="optional" order="1" />,
          <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$defaultID}" xlink:label="{$defaultLabel}" xlink:title="{normalize-space(())}" />
        )
      else (),

      (: dimension -> domain :)
      for $domain at $pos in $dimension.Members[]
      let $order as decimal :=
        if(empty($domain.Order) or $domain.Order eq null)
        then 1
        else $domain.Order
      let $domainID as string := replace($domain.Name, ":", "_")
      let $domainLabel as string := $domainID || "_" || $pos
      return (
        <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/dimension-domain" xlink:from="{$dimensionLabel}" xlink:to="{$domainLabel}" use="optional" order="{$order}" />,
        <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$domainID}" xlink:label="{$domainLabel}" xlink:title="{normalize-space($domain.Label)}" />,

        (: domain -> member :)
        concepts:tree-to-xbrl($domain, "http://xbrl.org/int/dim/arcrole/domain-member", $taxonomyName, 1, $pos)
      )
    )
  )
};

jsoniq version "1.0";
(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for manipulating hypercubes.

 :
 :
A hypercube provides a multi-dimensional structure to organize facts.

 :
 :
The dimensions of a hypercube correspond to XBRL aspects (concepts, entities, periods,
 : units, further XBRL dimensions). An XBRL hypercube only is made of XBRL dimensions, however
 : including the other aspects as well is very useful in the context of NOLAP, so that 28.io
 : does so.

 :
 :
28.io introduces the notion of default hypercube. A default hypercube is implicitly added
 : to each component, and only contains the concept, entity, period and unit aspects.
 : In other words, the default hypercube
 : of a component contains all these facts that do not have any XBRL dimensions.

 :
 :
With this module, you can retrieve all hypercubes contained in a component. You can
 : retrieve all facts contained in a hypercube (default dimension values are processed automatically),
 : either in raw form, or organized as a (2D) fact table. You can also populate a network (for example,
 : a presentation network) with the facts contained in a hypercube.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace facts = "http://28.io/modules/xbrl/facts";
import module namespace schema = "http://zorba.io/modules/schema";
import module namespace concepts = "http://28.io/modules/xbrl/concepts";

declare namespace xbrli = "http://www.xbrl.org/2003/instance";
declare namespace xlink = "http://www.w3.org/1999/xlink";
declare namespace link = "http://www.xbrl.org/2003/linkbase";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Joker for all archives.
 :)
declare variable $hypercubes:ALL_ARCHIVES as boolean := true;

(:~
 :
Retrieves all hypercubes in the supplied components.

 :
 : @param $components a sequence of components.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components($components as object*) as object*
{
  $components.Hypercubes ! values($$)
};

(:~
 :
Retrieves all hypercubes in the supplied components and
 : with the given names.

 :
 : @param $components a sequence of components.
 : @param $names a sequence of names.
 :
 : @return all hypercubes.
 :)
declare function hypercubes:hypercubes-for-components(
    $components as object*,
    $names as string*
) as object*
{
  $names ! $components.Hypercubes.($$)
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object
)
as object*
{
  facts:facts-for({Hypercube: $hypercube})
};

(:~
 :
 :
Retrieves all facts in the supplied hypercube, across all archives.

 :
Default values will be populated automatically.

 :
 :
 : @param $hypercube a hypercube.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts(
    $hypercube as object,
    $options as object?
)
as object*
{
  let $options as object := facts:merge-objects(
    { Hypercube: $hypercube },
    $options,
    true (: parameter has higher priority than option :)
  )
  return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*
)
as object*
{
  hypercubes:facts-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:facts-for-hypercube(
    $hypercube as object,
    $archives as item*,
    $options as object?
)
as item* (: object* :)
{
    if (exists($options.facts-for-archives-and-concepts))
    then
      let $options as object :=
        facts:merge-objects(
          { Hypercube: $hypercube },
          $options,
          true (: parameter has higher priority than option :)
        )
      let $facts-for-archives-and-concepts := $options.facts-for-archives-and-concepts
      let $concepts as string* :=
          distinct-values(descendant-objects($hypercube.Aspects.$facts:CONCEPT.Members).Name)
      let $concepts := if (exists($concepts))
                   then $concepts
                   else $facts:ALL_OF_THEM
      return
        $facts-for-archives-and-concepts(
        $archives, $concepts, $options)
    else
      let $options as object :=
        facts:merge-objects(
          {
            Hypercube: $hypercube,
            Filter: { $facts:ASPECTS : { $facts:ARCHIVE : [ archives:aid($archives) ] }}
          },
          $options,
          true (: parameter has higher priority than option :)
        )
      return facts:facts-for($options)
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*) as array
{
  hypercubes:fact-table-for-hypercube($hypercube, $archives, {})
};

(:~
 :
 :
Retrieves all facts from the supplied archives, that are relevant to the
 : supplied hypercube, and populates them with the default dimension values
 : when missing.

 :
 :
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube(
  $hypercube as object,
  $archives as item*,
  $options as object?) as array
{
  hypercubes:fact-table-for-hypercube-and-facts(
    $hypercube,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
Utility function to convert a sequence of facts to a fact table
 : given a hypercube. It is not recommended to use this function directly.
 : Use hypercubes:fact-table-for-hypercube instead.

 :
 :
 : @param $hypercube a hypercube.
 : @param $facts a sequence of facts that were retrieved with the hypercube.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:fact-table-for-hypercube-and-facts(
  $hypercube as object,
  $facts as object*) as array
{
  let $dimensions := distinct-values((
      $facts:CONCEPT,
      $facts:ENTITY,
      $facts:PERIOD,
      $facts:UNIT,
      keys($hypercube.Aspects)))
  return [
    [ $dimensions, "Value" ],
    for $fact in $facts
    return [
      $dimensions ! (let $member := $fact.Aspects.($$)
                     return if(exists($member))
                            then $member
                            else null),
      $fact.Value
    ]
  ]
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs or $hypercubes:ALL_ARCHIVES for no filtering.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*) as object*
{
  hypercubes:populate-networks-with-facts($networks, $hypercube, $archives, {})
};

(:~
 :
 :
Populates a concept-tree network with all facts from the supplied archives,
 : that are relevant to the
 : supplied hypercube. Default dimension values are added to the facts
 : when missing.

 :
 : @param $networks networks.
 : @param $hypercube a hypercube.
 : @param $archives a sequence of archives or their AIDs.
 : @param $options standard fact retrieving options.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-facts(
  $networks as object*,
  $hypercube as object,
  $archives as item*,
  $options as object?) as object*
{
  hypercubes:populate-networks-with-retrieved-facts(
    $networks,
    hypercubes:facts-for-hypercube($hypercube, $archives, $options)
  )
};

(:~
 :
 :
A utility function that populates networks with facts against concepts that belong
 : to them. It is not recommended to use this function directly.
 : Use hypercubes:populate-networks-with-retrieved-facts.

 :
 : @param $networks networks.
 : @param $facts a sequence of facts.
 :
 : @return a sequence of facts with populated dimension values.
 :)
declare function hypercubes:populate-networks-with-retrieved-facts(
  $networks as object*,
  $facts as object*) as object*
{
  let $fact-table-by-concepts := {|
    for $f in $facts
    group by $concept := $f.Aspects."xbrl:Concept"
    return { $concept : [ $f ] }
  |}
  return
    copy $result := $networks
    modify
      for $concept in descendant-objects($result.Trees)[exists($$.Name)]
      let $name := $concept.Name
      return insert json { "Facts" : $fact-table-by-concepts.$name } into $concept
    return $result
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited.

 :
 : @return dimensionless hypercube instantiation.
 :)
declare function hypercubes:dimensionless-hypercube() as object
{
  hypercubes:dimensionless-hypercube(())
};

(:~
 :
Returns an instantiation of a dimensionless Hypercube containing only the basic
 :    characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit).
 :    For each of those included aspects the value space is not limited, however
 :    you can restrict it using the options. If you need more
 :    XBRL dimensions, use the user-defined-hypercube function instead.

 :
 : @return dimensionless hypercube instantiation.
 : @param $options: additional options among which:
 : - Concepts: an array of concept names to include in the hypercube.
 : - Periods: an array of periods to include in the hypercube.
 : - Entities: an array of EIDs to include in the hypercube.
 : - Units: an array of units to include in the hypercube.
 :)
declare function hypercubes:dimensionless-hypercube($options as object?) as object
{
  hypercubes:user-defined-hypercube(
    {|
      if(exists($options.Concepts))
      then { "xbrl:Concept" : { Domain: $options.Concepts } }
      else (),
      if(exists($options.Entities))
      then { "xbrl:Entity" : { Domain: [ entities:eid($options.Entities[]) ] } }
      else (),
      if(exists($options.Periods))
      then { "xbrl:Period" : { Domain: $options.Periods } }
      else (),
      if(exists($options.Units))
      then { "xbrl:Unit" : { Domain: $options.Units } }
      else ()
    |}
  )
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?) as object
{
    hypercubes:user-defined-hypercube($dimensions, ())
};

(:~
 :
Returns an instantiation of a Hypercube containing the specified dimensions.
 :    The four basic characteristics (xbrl:Concept, xbrl:Period, xbrl:Entity, and xbrl:Unit)
 :    are always added, but can be overriden. For each of those included aspects the value space is,
 :    by default, not limited, and only xbrl:Unit has a default value.

 :
 : @return the hypercube instantiation.
 : @param $dimensions: an object with pairs of (dimension name, object with dimension information). These
 : dimension information subobjects
 : may have two (optional) fields: Default (a string) and Domain (an array of strings).
 : @param $options: an object with two options: Name (default: xbrl:UserDefinedHypercube) and Label
 : (default: User-defined Hypercube).
 :
 : @error hypercubes:invalid-type if a member of a typed dimension does not match the expected type
 : @error hypercubes:invalid-type if a member of an explicit dimension is not of type string
 :)
declare function hypercubes:user-defined-hypercube($dimensions as object?, $options as object?) as object
{
  {
    "Name" : ($options.Name, "xbrl:UserDefinedHypercube")[1],
    "Label" : ($options.Label, "User-defined Hypercube")[1],
    "IsOpen" : boolean($options.IsOpen),
    "Aspects" : {|
      for $dimension-name as string in distinct-values((
          "xbrl:Concept",
          "xbrl:Entity",
          "xbrl:Period",
          "xbrl:Unit",
          keys($dimensions)
      ))
      let $dimension-spec as object? := $dimensions.$dimension-name
      let $dimension-type as string? := $dimension-spec.Type
      let $dimension-domain as array? := $dimension-spec.Domain
      let $dimension-default as atomic? := $dimension-spec.Default
      let $domain-name := $dimension-name || "Domain"
      return {
        $dimension-name : {|
          (: Name :)
          {
            "Name" : $dimension-name
          },

          (: Label :)
          switch($dimension-name)
          case "xbrl:Concept" return {
            "Label" : "XBRL Concept Dimension"
          }
          case "xbrl:Entity" return {
            "Label" : "XBRL Entity Dimension"
          }
          case "xbrl:Period" return {
            "Label" : "XBRL Period Dimension"
          }
          case "xbrl:Unit" return {
            "Label" : "XBRL Unit Dimension"
          }
          default return (),

          (: Default value :)
          switch(true)
          case exists($dimension-default) return {
            "Default" :
              hypercubes:check-type-and-return($dimension-default,
                                               ($dimension-type, "string")[1])
          }
          case $dimension-name eq "xbrl:Unit" return {
            "Default" : "xbrl:NonNumeric"
          }
          default return (),

          (: Explicit vs. Typed :)
          {
            "Kind" : if(exists($dimension-type))
                     then "TypedDimension"
                     else "ExplicitDimension"
          },

          (: Type :)
          { "Type": $dimension-type }[exists($dimension-type)],

          (: Domain :)
          if (empty($dimension-type))
          then
          {
            "Members" : [
              for $dimension-member in distinct-values($dimension-domain[])
              return {
                  Name: hypercubes:check-type-and-return($dimension-member, "string")
              }
            ]
          }[exists($dimension-domain)]
          else {
            "DomainRestriction" : {
              "Name" : $domain-name,
              "Label" : $dimension-name || " Domain",
              "Enumeration" : [ hypercubes:check-type-and-return($dimension-domain[], $dimension-type) ]
            }
          }[exists($dimension-domain)]

        |}
      }
    |}
  }
};

declare %private function hypercubes:check-type-and-return(
    $dimension-members as atomic*,
    $dimension-type as string) as atomic*
{
  for $dimension-member as atomic in $dimension-members
  let $member-type as string := local-name-from-QName(schema:schema-type($dimension-member))
  let $expected-type as string := local-name-from-QName(xs:QName($dimension-type))
  return
  if (not $dimension-member instance of null
      and $member-type ne $dimension-type
      and ($member-type ne "int" or $expected-type ne "integer")
      )
  then error(
        xs:QName("hypercubes:invalid-type"),
        schema:schema-type($dimension-member) ||
          ": type doesn't match expected type (" || $dimension-type || ")")
  else $dimension-member
};

declare function hypercubes:modify-hypercube(
    $hypercube as object,
    $options as object?) as object
{
    let $options :=
    {|
        for $dimension in keys(($hypercube.Aspects, $options))
        let $hypercube-metadata := $hypercube.Aspects.$dimension
        let $new-metadata := $options.$dimension
        return {
            $dimension : if(exists($new-metadata))
            then
                $new-metadata
            else {|
                { Type : $hypercube.Aspects.$dimension.Type }[$hypercube-metadata.Kind eq "TypedDimension"],
                let $hypercube-domain :=
                  (
                     descendant-objects($hypercube-metadata.Members).Name,
                     jn:flatten($hypercube-metadata.DomainRestriction.Enumeration)
                  )
                where exists($hypercube-domain)
                return { Domain : [ distinct-values($hypercube-domain) ] },
                let $hypercube-default := $hypercube-metadata.Default
                where exists($hypercube-default)
                return { Default : $hypercube-default }
            |}
        }
    |}
    return hypercubes:user-defined-hypercube(
        $options,
        {
            Name: $hypercube.Name,
            Label: $hypercube.Label || " (with filter overrides)"
        }
    )
};

declare function hypercubes:merge($hypercubes as object*) as object
{
    hypercubes:user-defined-hypercube({|
        for $aspect in keys($hypercubes.Aspects)
        let $default := distinct-values($hypercubes.Aspects.$aspect.Default)
        let $domain := distinct-values((
            descendant-objects($hypercubes.Aspects.$aspect.Members).Name,
            $hypercubes.Aspects.$aspect.DomainRestriction.Enumeration[]
        ))
        let $kind as string? := distinct-values($hypercubes.Aspects.$aspect.Kind)
        let $type as string? := distinct-values($hypercubes.Aspects.$aspect.Type)
        return {
            $aspect : {|
                { Default: $default}[exists($default)],
                { Type : $type }[$kind eq "TypedDimension"],
                { Domain: [ distinct-values(($domain, $default)) ]}[exists($domain)]
            |}
        }
    |})
};

(:~
 :
Transforms a hypercube network - all, hypercube-dimension, dimension-default,
 :    dimension-domain, domain-member (except primary items).

 :
 : @param $hypercube the hypercube to transform.
 :
 : @return the XBRL locators and arcs to be put into an extended link.
 :)
declare function hypercubes:to-xbrl(
    $hypercube as object,
    $component as object,
    $taxonomyName as string
) as element()*
{
  let $hypercubeID as string := replace($hypercube.Name, ":", "_")
  let $hypercubeLocator as element() := <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$hypercubeID}" xlink:label="{$hypercubeID}" xlink:title="{normalize-space($hypercube.Label)}" />
  return (
    (: primary items -> hypercube :)
    for $rootConcept at $pos in $hypercube.Aspects."xbrl:Concept".Members[]
    let $order as decimal :=
        if(empty($rootConcept.Order) or $rootConcept.Order eq null)
        then 1
        else $rootConcept.Order
    let $fromID as string := replace($rootConcept.Name, ":", "_")
    let $fromLabel as string := $fromID || "_" || $pos
    return (
      <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$fromID}" xlink:label="{$fromLabel}" xlink:title="{normalize-space($rootConcept.Label)}" />,
      <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/all" xlink:from="{$fromLabel}" xlink:to="{$hypercubeID}" use="optional" order="{$order}" />
    ),
    $hypercubeLocator,

    (: hypercube -> dimension :)
    for $dimension at $pos in values($hypercube.Aspects)[not($$.Name = ("xbrl:Concept", "xbrl:Period", "xbrl:Unit", "xbrl:Entity"))]
    let $dimensionID as string := replace($dimension.Name, ":", "_")
    let $dimensionLabel as string := $dimensionID || "_" || $pos
    let $default as string? := $dimension.Default
    return (
      <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/hypercube-dimension" xlink:from="{$hypercubeID}" xlink:to="{$dimensionLabel}" use="optional" order="{$pos}" />,
      <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$dimensionID}" xlink:label="{$dimensionLabel}" xlink:title="{normalize-space($dimension.Label)}" />,

      (: dimension -> default :)
      if(exists($default))
      then
        let $defaultID as string := replace($default, ":", "_")
        let $defaultLabel as string := $defaultID || "_" || $pos
        return (
          <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/dimension-default" xlink:from="{$dimensionLabel}" xlink:to="{$defaultLabel}" use="optional" order="1" />,
          <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$defaultID}" xlink:label="{$defaultLabel}" xlink:title="{normalize-space(())}" />
        )
      else (),

      (: dimension -> domain :)
      for $domain at $pos in $dimension.Members[]
      let $order as decimal :=
        if(empty($domain.Order) or $domain.Order eq null)
        then 1
        else $domain.Order
      let $domainID as string := replace($domain.Name, ":", "_")
      let $domainLabel as string := $domainID || "_" || $pos
      return (
        <link:definitionArc xlink:type="arc" xlink:arcrole="http://xbrl.org/int/dim/arcrole/dimension-domain" xlink:from="{$dimensionLabel}" xlink:to="{$domainLabel}" use="optional" order="{$order}" />,
        <link:loc xlink:type="locator" xlink:href="{$taxonomyName}.xsd#{$domainID}" xlink:label="{$domainLabel}" xlink:title="{normalize-space($domain.Label)}" />,

        (: domain -> member :)
        concepts:tree-to-xbrl($domain, "http://xbrl.org/int/dim/arcrole/domain-member", $taxonomyName, 1, $pos)
      )
    )
  )
};


:bug: Fix regexp.

:bug: Fix regexp.

jsoniq version "1.0";

module namespace api = "http://apps.28.io/api";

import module namespace session    = "http://apps.28.io/session";
import module namespace sec-fiscal =
    "http://28.io/modules/xbrl/profiles/sec/fiscal/core";
import module namespace html =
    "http://www.zorba-xquery.com/modules/converters/html";
import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace request = "http://www.28msec.com/modules/http-request";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in api:flatten-json-object($objects, { KeepArray: true })
  return element { $root-name } {
    for $key in keys($flattened-object)
    let $value := $flattened-object.$key
    return typeswitch($value)
           case array return
             let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                                then $key
                                else $key || "s"
             let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                                then substring($key, 1, string-length($key) - 1)
                                else $key
             return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
           default return element { $key } { $value }
  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
    for $fact in $facts
    return {|
        {
            "Aspects" : {|
                trim($fact.Aspects, ("xbrl:Unit")),
                { "sec:Archive" : $fact.Aspects."xbrl28:Archive" }[exists($fact.Aspects."xbrl28:Archive")]
            |}
        },
        trim($fact, ("Aspects", "_id")),
        { Unit: $fact.Aspects."xbrl:Unit" }[exists($fact.Aspects."xbrl:Unit") and not exists($fact.Unit)]
    |}
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                             or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               then $token ! replace($$, "\n", "______28newline______")
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                               or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
                 then $token ! replace($$, ",", "______28comma______")
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return switch(true)
             case contains($cell, "http://") and
                  (contains($cell, request:server-name()) or
                   contains($cell, "rendering.secxbrl.info"))
             return element { $cell-tag-name } { Link }
             case starts-with(replace($cell, "(\r|\n| )+", ""), "<")
             return element { $cell-tag-name } {html:parse($cell)/*}
             default return element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};

jsoniq version "1.0";

module namespace api = "http://apps.28.io/api";

import module namespace session    = "http://apps.28.io/session";
import module namespace sec-fiscal =
    "http://28.io/modules/xbrl/profiles/sec/fiscal/core";
import module namespace html =
    "http://www.zorba-xquery.com/modules/converters/html";
import module namespace csv = "http://zorba.io/modules/json-csv";
import module namespace request = "http://www.28msec.com/modules/http-request";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in api:flatten-json-object($objects, { KeepArray: true })
  return element { $root-name } {
    for $key in keys($flattened-object)
    let $value := $flattened-object.$key
    return typeswitch($value)
           case array return
             let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                                then $key
                                else $key || "s"
             let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                                then substring($key, 1, string-length($key) - 1)
                                else $key
             return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
           default return element { $key } { $value }
  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
    for $fact in $facts
    return {|
        {
            "Aspects" : {|
                trim($fact.Aspects, ("xbrl:Unit")),
                { "sec:Archive" : $fact.Aspects."xbrl28:Archive" }[exists($fact.Aspects."xbrl28:Archive")]
            |}
        },
        trim($fact, ("Aspects", "_id")),
        { Unit: $fact.Aspects."xbrl:Unit" }[exists($fact.Aspects."xbrl:Unit") and not exists($fact.Unit)]
    |}
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                             or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               then $token ! replace($$, "\n", "______28newline______")
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                               or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
                 then $token ! replace($$, ",", "______28comma______")
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return switch(true)
             case contains($cell, "http://") and
                  (contains($cell, request:server-name()) or
                   contains($cell, "rendering.secxbrl.info"))
             return element { $cell-tag-name } { Link }
             case starts-with(replace($cell, "(\r|\n)", ""), "<")
             return element { $cell-tag-name } {html:parse($cell)/*}
             default return element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};


:green_heart: Fix warning.

:green_heart: Fix warning.

import module namespace config = "http://apps.28.io/config";
import module namespace api = "http://apps.28.io/api";
import module namespace session = "http://apps.28.io/session";
import module namespace backend = "http://apps.28.io/backend";

import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace components = "http://28.io/modules/xbrl/components";

import module namespace sec-filings = "http://28.io/modules/xbrl/profiles/sec/filings";
import module namespace sec-networks = "http://28.io/modules/xbrl/profiles/sec/networks";
import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

declare option rest:response "first-item";

declare function local:links($component as object) as object
{
  {
    FactTable: backend:url("facttable-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name
                    }),
    SpreadSheet: "http://rendering.secxbrl.info/#?url=" || encode-for-uri(
                backend:url("spreadsheet-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name,
                    eliminate: "true"
                })),
    ReportElements: backend:url("report-elements", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name
                }),
    ModelStructure: backend:url("modelstructure-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: "json",
                    role: $component.Role,
                    profile-name: $profile-name
                })
  }
};

declare function local:statistics($component as object) as object
{
  {
    NumRules: size($component.Rules),
    NumNetworks: components:num-networks($component),
    NumReportElements: components:num-report-elements($component),
    NumHypercubes: components:num-hypercubes($component),
    NumDimensions: components:num-dimensions($component),
    NumMembers: components:num-members($component),
    NumLineItems: components:num-line-items($component),
    NumAbstracts: components:num-abstracts($component),
    NumConcepts: components:num-concepts($component),
    Hypercubes: [ keys($component.Hypercubes) ]
  }
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token              as string? external;
declare  %rest:env                              variable $request-uri        as string  external;
declare  %rest:case-insensitive                 variable $format             as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode   as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker             as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear         as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod       as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind         as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $eid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $networkIdentifier  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $role               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $reportElement      as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $concept            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $label              as string* external;
declare  %rest:case-insensitive                 variable $profile-name       as string  external := $config:profile-name;

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $reportElement := ($reportElement, $concept)
let $networkIdentifier := distinct-values(($networkIdentifier, $role))

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic, ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object*  := entities:entities($archives.Entity)
let $components as object* :=
    multiplexer:components(
      $profile-name,
      $archives,
      $cid,
      $reportElement,
      $disclosure,
      $networkIdentifier,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"]
    )

let $res as object* :=
    switch($profile-name)
    case "sec" return
        for $component-group in $components
        let $disclosure := sec-networks:disclosures($component-group)
        where $disclosure ne "DefaultComponent"
        order by $component-group.Label
        group by $archive := $component-group.Archive
        let $archive := $archives[archives:aid($$) eq $archive]
        let $e := $entities[entities:eid($$) eq $archive.Entity]
        return
            {
               AccessionNumber : archives:aid($archive),
               EntityRegistrantName : $e.Profiles.SEC.CompanyName,
               CIK : entities:eid($e),
               FiscalYear :$archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
               FiscalPeriod :$archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus,
               AcceptanceDatetime : sec-filings:acceptance-dateTimes($archive),
               FormType : $archive.Profiles.SEC.FormType,
               Components : [
                 for $component in $component-group
                 return {|
                   sec-networks:summaries($component),
                   local:links($component),
                   local:statistics($component),
                   {
                     ValidationErrors: [ components:validation-errors($component) ]
                   }
                 |}
               ]
           }
    default return
        for $component in $components
        return {|
          project($component, ("Archive", "Label", "Role")),
          local:links($component),
          local:statistics($component),
          {
            ValidationErrors: [ components:validation-errors($component) ]
          },
          { Disclosure: $component.Profiles.FSA.Disclosure}[$profile-name eq "japan"]
        |}
let $result := switch($profile-name)
               case "sec"
               return { Archives: [ $res ] }
               default return { Components : [ $res ] }
let $comment :=
 {
    NumComponents : count($components),
    TotalNumComponents: session:num-components(),
    TotalNumArchives: session:num-archives()
}
let $serializers := {
    to-xml : switch($profile-name)
        case "sec"
        return function($res as object) as node() {
        <Archives>{
                  for $r in flatten($res.Archives)
                  return
                      <Archive id="{$r.AccessionNumber}">
                         <EntityRegistrantName>{$r.EntityRegistrantName}</EntityRegistrantName>
                         <CIK>{$r.CIK}</CIK>
                         <FiscalYear>{$r.FiscalYear}</FiscalYear>
                         <FiscalPeriod>{$r.FiscalPeriod}</FiscalPeriod>
                         <AcceptanceDatetime>{$r.AcceptanceDatetime}</AcceptanceDatetime>
                         <FormType>{$r.FormType}</FormType>
                         <Components>{
                             api:json-to-xml($r.Components[], "Component")
                         }</Components>
                     </Archive>
             }</Archives>
        }
        default return function($res as object) as node() {
        <Components>{
          api:json-to-xml($res.Components[], "Component")
        }</Components>
    },
    to-csv : function($res as object) as string {
        switch($profile-name)
        case "sec" return api:json-to-csv($res.Archives[].Components[])
        default return api:json-to-csv($res.Components[])
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "components")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       default return api:check-and-return-results($token, $results, $format)

import module namespace config = "http://apps.28.io/config";
import module namespace api = "http://apps.28.io/api";
import module namespace session = "http://apps.28.io/session";
import module namespace backend = "http://apps.28.io/backend";

import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace components = "http://28.io/modules/xbrl/components";

import module namespace sec-filings = "http://28.io/modules/xbrl/profiles/sec/filings";
import module namespace sec-networks = "http://28.io/modules/xbrl/profiles/sec/networks";
import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";

declare option rest:response "first-item";

declare function local:links($component as object)
{
  {
    FactTable: backend:url("facttable-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name
                    }),
    SpreadSheet: "http://rendering.secxbrl.info/#?url=" || encode-for-uri(
                backend:url("spreadsheet-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name,
                    eliminate: "true"
                })),
    ReportElements: backend:url("report-elements", {
                    token: $token,
                    aid: $component.Archive,
                    format: $format,
                    role: $component.Role,
                    profile-name: $profile-name
                }),
    ModelStructure: backend:url("modelstructure-for-component", {
                    token: $token,
                    aid: $component.Archive,
                    format: "json",
                    role: $component.Role,
                    profile-name: $profile-name
                })
  }
};

declare function local:statistics($component as object)
{
  {
    NumRules: size($component.Rules),
    NumNetworks: components:num-networks($component),
    NumReportElements: components:num-report-elements($component),
    NumHypercubes: components:num-hypercubes($component),
    NumDimensions: components:num-dimensions($component),
    NumMembers: components:num-members($component),
    NumLineItems: components:num-line-items($component),
    NumAbstracts: components:num-abstracts($component),
    NumConcepts: components:num-concepts($component),
    Hypercubes: [ keys($component.Hypercubes) ]
  }
};

(: Query parameters :)
declare  %rest:case-insensitive                 variable $token              as string? external;
declare  %rest:env                              variable $request-uri        as string  external;
declare  %rest:case-insensitive                 variable $format             as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode   as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker             as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear         as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod       as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $filingKind         as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $eid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $networkIdentifier  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $role               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $reportElement      as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $concept            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $label              as string* external;
declare  %rest:case-insensitive                 variable $profile-name       as string  external := $config:profile-name;

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $reportElement := ($reportElement, $concept)
let $networkIdentifier := distinct-values(($networkIdentifier, $role))

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic, ())

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object*  := entities:entities($archives.Entity)
let $components as object* :=
    multiplexer:components(
      $profile-name,
      $archives,
      $cid,
      $reportElement,
      $disclosure,
      $networkIdentifier,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"]
    )

let $res as object* :=
    switch($profile-name)
    case "sec" return
        for $component-group in $components
        let $disclosure := sec-networks:disclosures($component-group)
        where $disclosure ne "DefaultComponent"
        order by $component-group.Label
        group by $archive := $component-group.Archive
        let $archive := $archives[archives:aid($$) eq $archive]
        let $e := $entities[entities:eid($$) eq $archive.Entity]
        return
            {
               AccessionNumber : archives:aid($archive),
               EntityRegistrantName : $e.Profiles.SEC.CompanyName,
               CIK : entities:eid($e),
               FiscalYear :$archive.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
               FiscalPeriod :$archive.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus,
               AcceptanceDatetime : sec-filings:acceptance-dateTimes($archive),
               FormType : $archive.Profiles.SEC.FormType,
               Components : [
                 for $component in $component-group
                 return {|
                   sec-networks:summaries($component),
                   local:links($component),
                   local:statistics($component),
                   {
                     ValidationErrors: [ components:validation-errors($component) ]
                   }
                 |}
               ]
           }
    default return
        for $component in $components
        return {|
          project($component, ("Archive", "Label", "Role")),
          local:links($component),
          local:statistics($component),
          {
            ValidationErrors: [ components:validation-errors($component) ]
          },
          { Disclosure: $component.Profiles.FSA.Disclosure}[$profile-name eq "japan"]
        |}
let $result := switch($profile-name)
               case "sec"
               return { Archives: [ $res ] }
               default return { Components : [ $res ] }
let $comment :=
 {
    NumComponents : count($components),
    TotalNumComponents: session:num-components(),
    TotalNumArchives: session:num-archives()
}
let $serializers := {
    to-xml : switch($profile-name)
        case "sec"
        return function($res as object) as node() {
        <Archives>{
                  for $r in flatten($res.Archives)
                  return
                      <Archive id="{$r.AccessionNumber}">
                         <EntityRegistrantName>{$r.EntityRegistrantName}</EntityRegistrantName>
                         <CIK>{$r.CIK}</CIK>
                         <FiscalYear>{$r.FiscalYear}</FiscalYear>
                         <FiscalPeriod>{$r.FiscalPeriod}</FiscalPeriod>
                         <AcceptanceDatetime>{$r.AcceptanceDatetime}</AcceptanceDatetime>
                         <FormType>{$r.FormType}</FormType>
                         <Components>{
                             api:json-to-xml($r.Components[], "Component")
                         }</Components>
                     </Archive>
             }</Archives>
        }
        default return function($res as object) as node() {
        <Components>{
          api:json-to-xml($res.Components[], "Component")
        }</Components>
    },
    to-csv : function($res as object) as string {
        switch($profile-name)
        case "sec" return api:json-to-csv($res.Archives[].Components[])
        default return api:json-to-csv($res.Components[])
    }
}

let $results := api:serialize($result, $comment, $serializers, $format, "components")
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       default return api:check-and-return-results($token, $results, $format)


:green_heart: Fix warning.

:green_heart: Fix warning.

jsoniq version "1.0";
(:
 : Copyright 2014 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving concepts
 : and labels.

 :
 : @author Federico Cavalieri
 :)
module namespace concepts = "http://28.io/modules/xbrl/concepts";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Name of the collection the concepts are stored in.
 :)
declare variable $concepts:col as xs:string := "concepts";

(:~
 : Name of the field pointing to the archive.
 :)
declare variable $concepts:ARCHIVE as xs:string := "Archive";

(:~
 : Name of the field pointing to the role.
 :)
declare variable $concepts:ROLE as xs:string := "Role";

(:~
 : Name of the field pointing to the name.
 :)
declare variable $concepts:NAME as xs:string := "Name";

(:~
 : Name of the field pointing to the labels.
 :)
declare variable $concepts:LABELS as xs:string := "Labels";

(:~
 : The default component link role.
 :)
declare variable $concepts:DEFAULT_COMPONENT_LINK_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/link";

(:~
 : Allow any component link role
 :)
declare variable $concepts:ANY_COMPONENT_LINK_ROLE as xs:string :=
    "*";

(:~
 : A joker to ask for all concept names.
 :)
declare variable $concepts:ALL_CONCEPT_NAMES as xs:string := "";

(:~
 :
Retrieves all concepts.

 :
 : @return all concepts.
 :)
declare function concepts:concepts() as object*
{
  mw:find($concepts:col,{})
};

(:~
 :
Retrieves the concepts which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 : @param $concept-names the concepts names.
 : @param $archives the archive numbers.
 : @param $component-roles the component roles.
 :
 : @return the matching concepts.
 :)
declare function concepts:concepts(
    $concept-names as string*,
    $archives as string*,
    $component-roles as string*
  ) as object*
{
  if (exists($archives))
  then mw:find($concepts:col,
    {|
      {
        $concepts:ARCHIVE : { "$in" : [ $archives ] }
      },
      {
        $concepts:ROLE : { "$in" :
          [ $component-roles, "http://www.xbrl.org/2003/role/link" ]
        }
      }[not $component-roles = $concepts:ANY_COMPONENT_LINK_ROLE],
      {
        $concepts:NAME : { "$in" : [ $concept-names ] }
      }[not $concept-names = $concepts:ALL_CONCEPT_NAMES]
    |}
  )
  else ()
};

jsoniq version "1.0";
(:
 : Copyright 2014 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving concepts
 : and labels.

 :
 : @author Federico Cavalieri
 :)
module namespace concepts = "http://28.io/modules/xbrl/concepts";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

import module namespace components = "http://28.io/modules/xbrl/components";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Name of the collection the concepts are stored in.
 :)
declare variable $concepts:col as xs:string := "concepts";

(:~
 : Name of the field pointing to the archive.
 :)
declare variable $concepts:ARCHIVE as xs:string := "Archive";

(:~
 : Name of the field pointing to the role.
 :)
declare variable $concepts:ROLE as xs:string := "Role";

(:~
 : Name of the field pointing to the name.
 :)
declare variable $concepts:NAME as xs:string := "Name";

(:~
 : Name of the field pointing to the labels.
 :)
declare variable $concepts:LABELS as xs:string := "Labels";

(:~
 : The default component link role.
 :)
declare variable $concepts:DEFAULT_COMPONENT_LINK_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/link";

(:~
 : Allow any component link role
 :)
declare variable $concepts:ANY_COMPONENT_LINK_ROLE as xs:string :=
    "*";

(:~
 : A joker to ask for all concept names.
 :)
declare variable $concepts:ALL_CONCEPT_NAMES as xs:string := "";

(:~
 :
Retrieves all concepts.

 :
 : @return all concepts.
 :)
declare function concepts:concepts() as object*
{
  mw:find($concepts:col,{})
};

(:~
 :
Retrieves the concepts which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 : @param $concept-names the concepts names.
 : @param $archives the archive numbers.
 : @param $component-roles the component roles.
 :
 : @return the matching concepts.
 :)
declare function concepts:concepts(
    $concept-names as string*,
    $archives as string*,
    $component-roles as string*
  ) as object*
{
  if (exists($archives))
  then mw:find($concepts:col,
    {|
      {
        $concepts:ARCHIVE : { "$in" : [ $archives ] }
      },
      {
        $concepts:ROLE : { "$in" :
          [ $component-roles, "http://www.xbrl.org/2003/role/link" ]
        }
      }[not $component-roles = $concepts:ANY_COMPONENT_LINK_ROLE],
      {
        $concepts:NAME : { "$in" : [ $concept-names ] }
      }[not $concept-names = $concepts:ALL_CONCEPT_NAMES]
    |}
  )
  else ()
};