function generateProperties(spec) {
  const layouts = spec.layout;
  const paints = spec.paint;
  var codes = {};
  var docs = {};
  var enums = {};
  layouts.forEach(l => {
    const layerType = titleCase(l.split('_')[1])
    docs[layerType] = [];
    codes[layerType] = [];
    Object.entries(spec[l]).forEach(([name, prop]) => {
      if (name == 'visibility') return '';
      if (prop.type === 'enum') {
        enums[name] = Object.keys(prop.values).join(' | ');
      }
      codes[layerType].push(generateElmProperty(name, prop, layerType, 'Layout'));
      docs[layerType].push(camelCase(name));
    })
  })
  paints.forEach(l => {
    const layerType = titleCase(l.split('_')[1])
    Object.entries(spec[l]).forEach(([name, prop]) => {
      if (name == 'visibility') return '';
      if (prop.type === 'enum') {
        enums[name] = Object.keys(prop.values).join(' | ');
      }
      codes[layerType].push(generateElmProperty(name, prop, layerType, 'Paint'))
      docs[layerType].push(camelCase(name));
    })
  })
  Object.values(docs).forEach(d => d.sort())
  Object.values(codes).forEach(d => d.sort());
  console.log(enums);
  return `
module Mapbox.Layer exposing (
  Layer, SourceId, Background, Fill, Symbol, Line, Raster, Circle, FillExtrusion, Heatmap, Hillshade, LayerAttr,
  encode,
  background, fill, symbol, line, raster, circle, fillExtrusion, heatmap, hillshade,
  metadata, source, sourceLayer, minzoom, maxzoom, filter, visible,
  ${Object.values(docs).map(d => d.join(', ')).join(',\n  ')})
{-|
Layers specify what is actually rendered on the map and are rendered in order.

Except for layers of the background type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled.

There are two kinds of properties: *Layout* and *Paint* properties.

Layout properties are applied early in the rendering process and define how data for that layer is passed to the GPU. Changes to a layout property require an asynchronous "layout" step.

Paint properties are applied later in the rendering process. Changes to a paint property are cheap and happen synchronously.


### Working with layers

@docs Layer, SourceId, encode

### Layer Types

@docs background, fill, symbol, line, raster, circle, fillExtrusion, heatmap, hillshade
@docs Background, Fill, Symbol, Line, Raster, Circle, FillExtrusion, Heatmap, Hillshade

### General Attributes

@docs LayerAttr
@docs metadata, source, sourceLayer, minzoom, maxzoom, filter, visible

${Object.entries(docs).map(([section, docs]) => `### ${section} Attributes\n\n@docs ${docs.join(', ')}`).join('\n\n')}
-}

import Array exposing (Array)
import Json.Decode
import Json.Encode as Encode exposing (Value)
import Mapbox.Expression as Expression exposing (Anchor, CameraExpression, Color, DataExpression, Expression, LineJoin)

{-| Represents a layer. -}
type Layer
    = Layer Value

{-| All layers (except background layers) need a source -}
type alias SourceId = String

{-| -}
type Background
    = BackgroundLayer

{-| -}
type Fill
    = FillLayer

{-| -}
type Symbol
    = SymbolLayer

{-| -}
type Line
    = LineLayer

{-| -}
type Raster
    = RasterLayer

{-| -}
type Circle
    = CircleLayer

{-| -}
type FillExtrusion
    = FillExtrusionLayer

{-| -}
type Heatmap
    = HeatmapLayer

{-| -}
type Hillshade
    = HillshadeLayer

{-| Turns a layer into JSON -}
encode : Layer -> Value
encode (Layer value) =
    value




layerImpl tipe source id attrs =
    [ ( "type", Encode.string tipe )
    , ( "id", Encode.string id )
    , ( "source", Encode.string source)
    ]
        ++ encodeAttrs attrs
        |> Encode.object
        |> Layer


encodeAttrs attrs =
    let
        { top, layout, paint } =
            List.foldl
                (\\attr ({ top, layout, paint } as lists) ->
                    case attr of
                        Top key val ->
                            { lists | top = ( key, val ) :: top }

                        Paint key val ->
                            { lists | paint = ( key, val ) :: paint }

                        Layout key val ->
                            { lists | layout = ( key, val ) :: layout }
                )
                { top = [], layout = [], paint = [] }
                attrs
    in
        ( "layout", Encode.object layout ) :: ( "paint", Encode.object paint ) :: top

{-| The background color or pattern of the map. -}
background : String -> List (LayerAttr Background) -> Layer
background tipe id attrs =
    [ ( "type", Encode.string "background" )
    , ( "id", Encode.string id )
    ]
        ++ encodeAttrs attrs
        |> Encode.object
        |> Layer

{-| A filled polygon with an optional stroked border. -}
fill : String -> SourceId -> List (LayerAttr Fill) -> Layer
fill =
    layerImpl "fill"

{-| A stroked line. -}
line : String  -> SourceId -> List (LayerAttr Line) -> Layer
line =
    layerImpl "line"

{-| An icon or a text label. -}
symbol : String  -> SourceId -> List (LayerAttr Symbol) -> Layer
symbol =
    layerImpl "symbol"

{-| Raster map textures such as satellite imagery. -}
raster : String -> SourceId -> List (LayerAttr Raster) -> Layer
raster =
    layerImpl "raster"

{-| A filled circle. -}
circle : String -> SourceId -> List (LayerAttr Circle) -> Layer
circle =
    layerImpl "circle"

{-| An extruded (3D) polygon. -}
fillExtrusion : String -> SourceId -> List (LayerAttr FillExtrusion) -> Layer
fillExtrusion =
    layerImpl "fill-extrusion"

{-| A heatmap. -}
heatmap : String -> SourceId -> List (LayerAttr Heatmap) -> Layer
heatmap =
    layerImpl "heatmap"

{-| Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB and Mapzen Terrarium tiles. -}
hillshade : String -> SourceId -> List (LayerAttr Hillshade) -> Layer
hillshade =
    layerImpl "hillshade"

{-| -}
type LayerAttr tipe
    = Top String Value
    | Paint String Value
    | Layout String Value



-- General Attributes

{-| Arbitrary properties useful to track with the layer, but do not influence rendering. Properties should be prefixed to avoid collisions, like 'mapbox:'. -}
metadata : Value -> LayerAttr all
metadata =
    Top "metadata"


{-| Layer to use from a vector tile source. Required for vector tile sources; prohibited for all other source types, including GeoJSON sources. -}
sourceLayer : String -> LayerAttr all
sourceLayer =
    Encode.string >> Top "source-layer"

{-| The minimum zoom level for the layer. At zoom levels less than the minzoom, the layer will be hidden. A number between 0 and 24 inclusive. -}
minzoom : Float -> LayerAttr all
minzoom =
    Encode.float >> Top "minzoom"

{-| The maximum zoom level for the layer. At zoom levels equal to or greater than the maxzoom, the layer will be hidden. A number between 0 and 24 inclusive. -}
maxzoom : Float -> LayerAttr all
maxzoom =
    Encode.float >> Top "maxzoom"

{-| A expression specifying conditions on source features. Only features that match the filter are displayed. -}
filter : Expression any Bool -> LayerAttr all
filter =
    Expression.encode >> Top "filter"

{-| Whether this layer is displayed. -}
visible : Expression CameraExpression Bool -> LayerAttr any
visible vis =
    Layout "visibility" <| Expression.encode <| Expression.ifElse vis (Expression.str "visible") (Expression.str "none")

${Object.entries(codes).map(([section, codes]) => `-- ${section}\n\n${codes.join('\n')}`).join('\n\n')}
`
}

function requires(req) {
  if (typeof req === 'string') {
      return `Requires \`${camelCase(req)}\`.`;
  } else if (req['!']) {
      return `Disabled by \`${camelCase(req['!'])}\`.`;
  } else if (req['<=']) {
      return `Must be less than or equal to \`${camelCase(req['<='])}\`.`;
  } else {
      const [name, value] = Object.entries(req)[0];
      if (Array.isArray(value)) {
          return `Requires \`${camelCase(name)}\` to be ${
              value
                  .reduce((prev, curr) => [prev, ', or ', curr])}.`;
      } else {
          return `Requires \`${camelCase(name)}\` to be \`${value}\`.`;
      }
  }
}

function generateElmProperty(name, prop, layerType, position) {
  if (name == 'visibility') return ''
  if (prop['property-type'] === 'constant') throw "Constant property type not supported";
  const elmName = camelCase(name);
  const exprKind = prop['sdk-support']['data-driven styling'] &&  prop['sdk-support']['data-driven styling'].js ? 'any' : 'CameraExpression';
  const exprType = getElmType(prop);
  let bounds = '';
  if ('minimum' in prop && 'maximum' in prop) {
    bounds = `\n\nShould be between \`${prop.minimum}\` and \`${prop.maximum}\` inclusive. `
  } else if ('minimum' in prop) {
    bounds = `\n\nShould be greater than or equal to \`${prop.minimum}\`. `
  } else if ('maximum' in prop) {
    bounds = `\n\nShould be less than or equal to \`${prop.maximum}\`. `
  }
  return `
{-| ${prop.doc.replace(/`(\w+\-.+?)`/g, str => '`' + camelCase(str.substr(1)))} ${position} property. ${bounds}${prop.units ? `\nUnits in ${prop.units}. ` : ''}${prop.default !== undefined ? 'Defaults to `' + prop.default + '`. ' : ''}${prop.requires ? prop.requires.map(requires).join(' ') : ''}
-}
${elmName} : Expression ${exprKind} ${exprType} -> LayerAttr ${layerType}
${elmName} =
    Expression.encode >> ${position} "${name}"`
}

function getElmType({type, value, values}) {
  switch(type) {
    case 'number':
      return 'Float';
    case 'boolean':
      return "Bool";
    case 'string':
      return 'String';
    case 'color':
      return 'Color';
    case 'array':
      switch(value) {
        case 'number':
          return '(Array Float)';
        case 'string':
          return '(Array String)';
      }
    case 'enum':
      switch(Object.keys(values).join(' | ')) {
        case "map | viewport":
          return 'Anchor';
        case "map | viewport | auto":
          return 'AnchorAuto';
        case "center | left | right | top | bottom | top-left | top-right | bottom-left | bottom-right":
          return 'Position';
        case 'none | width | height | both':
          return 'TextFit';
        case 'butt | round | square':
          return 'LineCap';
        case 'bevel | round | miter':
          return 'LineJoin';
        case 'point | line':
          return 'SymbolPlacement';
        case 'left | center | right':
          return 'TextJustify';
        case 'none | uppercase | lowercase':
          return 'TextTransform';
      }
  }
  throw `Unknown type ${type}`
}

function titleCase(str) {
  return str.replace(/\-/, ' ').replace(
        /\w\S*/g,
        function(txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        }
    ).replace(/\s/, '');
  }

function camelCase(str) {
  return str.replace(/(?:^\w|[A-Z]|\b\w|\-\w)/g, function(letter, index) {
    return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
  }).replace(/(?:\s|\-)+/g, '');
}


function makeSignatures(name, constants) {
  return `{-| -}
type ${name} = ${name}

  ${constants.split(' | ').map(c => `
{-| -}
${camelCase(name + ' ' + c)} : Expression exprType ${name}
${camelCase(name + ' ' + c)} = Expression (Json.Encode.string "${c}")
`).join('\n')}`
}